zidane 5.6.14 → 5.7.4
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/README.md +3 -1
- package/dist/{agent-ClkpElCZ.d.ts → agent-BNS2nx_T.d.ts} +535 -15
- package/dist/agent-BNS2nx_T.d.ts.map +1 -0
- package/dist/chat/pure.d.ts +4 -0
- package/dist/chat/pure.js +3 -0
- package/dist/chat.d.ts +31 -661
- package/dist/chat.d.ts.map +1 -1
- package/dist/chat.js +5 -3
- package/dist/chat.js.map +1 -1
- package/dist/contexts/docker.d.ts +1 -1
- package/dist/contexts/docker.d.ts.map +1 -1
- package/dist/contexts/docker.js.map +1 -1
- package/dist/{contexts-BOtMvzli.js → contexts-BD2U_xpi.js} +2 -2
- package/dist/{contexts-BOtMvzli.js.map → contexts-BD2U_xpi.js.map} +1 -1
- package/dist/contexts.d.ts +3 -3
- package/dist/contexts.js +1 -1
- package/dist/edit-utils-DnfNoj16.js +574 -0
- package/dist/edit-utils-DnfNoj16.js.map +1 -0
- package/dist/{errors-DdZXnyXE.js → errors-CoQnKRf1.js} +32 -2
- package/dist/{errors-DdZXnyXE.js.map → errors-CoQnKRf1.js.map} +1 -1
- package/dist/fetch-url-CPxfiXDa.js +518 -0
- package/dist/fetch-url-CPxfiXDa.js.map +1 -0
- package/dist/image-sniff-B7uFSNO1.js +90 -0
- package/dist/image-sniff-B7uFSNO1.js.map +1 -0
- package/dist/{index-CbS75MD3.d.ts → index-CZOwAJIX.d.ts} +2 -2
- package/dist/index-CZOwAJIX.d.ts.map +1 -0
- package/dist/{index-CTDMMdIy.d.ts → index-Ck_AWt8P.d.ts} +3 -4
- package/dist/index-Ck_AWt8P.d.ts.map +1 -0
- package/dist/{index-v3Tzobqr.d.ts → index-KiS7w0dC.d.ts} +3 -3
- package/dist/index-KiS7w0dC.d.ts.map +1 -0
- package/dist/index.d.ts +6 -6
- package/dist/index.js +13 -12
- package/dist/index.js.map +1 -1
- package/dist/{interpolate-DM1UcKeQ.js → interpolate-TySiqKzc.js} +23 -23
- package/dist/{interpolate-DM1UcKeQ.js.map → interpolate-TySiqKzc.js.map} +1 -1
- package/dist/{login-7tHcckmX.js → login-BDeqENSe.js} +7 -58
- package/dist/login-BDeqENSe.js.map +1 -0
- package/dist/{mcp-DGeB7-3D.js → mcp-Kqzz-Rs_.js} +8 -6
- package/dist/mcp-Kqzz-Rs_.js.map +1 -0
- package/dist/mcp.d.ts +2 -2
- package/dist/mcp.js +1 -1
- package/dist/{messages-Dym8S_YH.js → messages-CvRQTdbR.js} +118 -39
- package/dist/messages-CvRQTdbR.js.map +1 -0
- package/dist/{presets-w9Px_aAm.js → presets-JuOnSI-i.js} +2 -2
- package/dist/{presets-w9Px_aAm.js.map → presets-JuOnSI-i.js.map} +1 -1
- package/dist/presets.d.ts +3 -3
- package/dist/presets.js +1 -1
- package/dist/{providers-beXyD9W9.js → providers-h4HJPbbv.js} +485 -31
- package/dist/providers-h4HJPbbv.js.map +1 -0
- package/dist/providers.d.ts +2 -2
- package/dist/providers.js +3 -3
- package/dist/restate.d.ts +1 -1
- package/dist/restate.d.ts.map +1 -1
- package/dist/restate.js.map +1 -1
- package/dist/session/sqlite.d.ts +1 -1
- package/dist/session/sqlite.d.ts.map +1 -1
- package/dist/session/sqlite.js +1 -1
- package/dist/session/sqlite.js.map +1 -1
- package/dist/{session-BRIsmBSY.js → session-BzLou2_-.js} +2 -2
- package/dist/{session-BRIsmBSY.js.map → session-BzLou2_-.js.map} +1 -1
- package/dist/session.d.ts +2 -2
- package/dist/session.js +2 -2
- package/dist/skills.d.ts +3 -3
- package/dist/skills.js +1 -1
- package/dist/skills.js.map +1 -1
- package/dist/{stats-Lc3zL3RM.js → stats-DAKBEKjc.js} +12 -2
- package/dist/stats-DAKBEKjc.js.map +1 -0
- package/dist/{stdio-loader-EVAF5KlU.js → stdio-loader-Ce68wUmM.js} +4 -4
- package/dist/stdio-loader-Ce68wUmM.js.map +1 -0
- package/dist/tool-formatters-CU-j3a3e.d.ts +1471 -0
- package/dist/tool-formatters-CU-j3a3e.d.ts.map +1 -0
- package/dist/tools/fetch-url.d.ts +70 -0
- package/dist/tools/fetch-url.d.ts.map +1 -0
- package/dist/tools/fetch-url.js +2 -0
- package/dist/tools/web-search.d.ts +7 -0
- package/dist/tools/web-search.d.ts.map +1 -0
- package/dist/tools/web-search.js +190 -0
- package/dist/tools/web-search.js.map +1 -0
- package/dist/{tools-DhrLrOEr.js → tools-BGtJK0vo.js} +1368 -421
- package/dist/tools-BGtJK0vo.js.map +1 -0
- package/dist/tools.d.ts +3 -3
- package/dist/tools.js +1 -1
- package/dist/{turn-operations-UAkOjO-u.js → transcript-anchors-BTSZAPVc.js} +147 -2713
- package/dist/transcript-anchors-BTSZAPVc.js.map +1 -0
- package/dist/{transcript-anchors-D0TR6djV.d.ts → transcript-anchors-DX90kXc4.d.ts} +13 -1299
- package/dist/transcript-anchors-DX90kXc4.d.ts.map +1 -0
- package/dist/tui.d.ts +58 -28
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +1349 -422
- package/dist/tui.js.map +1 -1
- package/dist/turn-operations-CCHfR9eC.js +1938 -0
- package/dist/turn-operations-CCHfR9eC.js.map +1 -0
- package/dist/turn-operations-DDIl4YVk.d.ts +658 -0
- package/dist/turn-operations-DDIl4YVk.d.ts.map +1 -0
- package/dist/{types-oKPBdCmL.js → types-BPw_i5vb.js} +1 -1
- package/dist/types-BPw_i5vb.js.map +1 -0
- package/dist/{types-KukEp-mi.d.ts → types-CEAMIUXw.d.ts} +1 -1
- package/dist/types-CEAMIUXw.d.ts.map +1 -0
- package/dist/types.d.ts +4 -4
- package/dist/types.js +3 -3
- package/docs/CHAT.md +53 -6
- package/docs/SKILL.md +3 -0
- package/docs/TUI.md +7 -0
- package/package.json +18 -2
- package/dist/agent-ClkpElCZ.d.ts.map +0 -1
- package/dist/index-CTDMMdIy.d.ts.map +0 -1
- package/dist/index-CbS75MD3.d.ts.map +0 -1
- package/dist/index-v3Tzobqr.d.ts.map +0 -1
- package/dist/login-7tHcckmX.js.map +0 -1
- package/dist/mcp-DGeB7-3D.js.map +0 -1
- package/dist/messages-Dym8S_YH.js.map +0 -1
- package/dist/providers-beXyD9W9.js.map +0 -1
- package/dist/stats-Lc3zL3RM.js.map +0 -1
- package/dist/stdio-loader-EVAF5KlU.js.map +0 -1
- package/dist/tools-DhrLrOEr.js.map +0 -1
- package/dist/transcript-anchors-D0TR6djV.d.ts.map +0 -1
- package/dist/turn-operations-UAkOjO-u.js.map +0 -1
- package/dist/types-KukEp-mi.d.ts.map +0 -1
- package/dist/types-oKPBdCmL.js.map +0 -1
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
import { l as errorMessage } from "./errors-CoQnKRf1.js";
|
|
2
|
+
import { Buffer } from "node:buffer";
|
|
3
|
+
import { request } from "node:http";
|
|
4
|
+
import { promises } from "node:dns";
|
|
5
|
+
import { request as request$1 } from "node:https";
|
|
6
|
+
import { isIP } from "node:net";
|
|
7
|
+
//#region src/tools/_html.ts
|
|
8
|
+
/**
|
|
9
|
+
* Shared HTML → plain-text helpers used by the web-egress tools (`web_search`
|
|
10
|
+
* snippet extraction, `fetch_url` body decoding).
|
|
11
|
+
*
|
|
12
|
+
* Best-effort by contract: a real HTML parser would be more correct but the
|
|
13
|
+
* payload is always model context, never user-facing rendered output, so a
|
|
14
|
+
* tag-stripper plus a curated entity table covers the practical cases
|
|
15
|
+
* (Wikipedia, GitHub READMEs, MDN docs, Stack Overflow answers). When a tool
|
|
16
|
+
* needs richer parsing it should reach for an actual parser rather than
|
|
17
|
+
* extending these helpers ad-hoc.
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* Curated named-entity table. Kept narrow on purpose — the long tail
|
|
21
|
+
* (`Å`, `×`, mathematical refs, …) is out of scope because the
|
|
22
|
+
* model handles unknown entities fine when they pass through verbatim. Add
|
|
23
|
+
* entries when an entity is observed mangling real output, not speculatively.
|
|
24
|
+
*
|
|
25
|
+
* Keys are stored without the leading `&` / trailing `;` and matched
|
|
26
|
+
* case-insensitively by the decoder.
|
|
27
|
+
*/
|
|
28
|
+
const NAMED_ENTITIES = {
|
|
29
|
+
amp: "&",
|
|
30
|
+
apos: "'",
|
|
31
|
+
bull: "•",
|
|
32
|
+
copy: "©",
|
|
33
|
+
ensp: " ",
|
|
34
|
+
emsp: " ",
|
|
35
|
+
gt: ">",
|
|
36
|
+
hellip: "…",
|
|
37
|
+
laquo: "«",
|
|
38
|
+
ldquo: "“",
|
|
39
|
+
lsquo: "‘",
|
|
40
|
+
lt: "<",
|
|
41
|
+
mdash: "—",
|
|
42
|
+
middot: "·",
|
|
43
|
+
nbsp: " ",
|
|
44
|
+
ndash: "–",
|
|
45
|
+
quot: "\"",
|
|
46
|
+
raquo: "»",
|
|
47
|
+
rdquo: "”",
|
|
48
|
+
reg: "®",
|
|
49
|
+
rsquo: "’",
|
|
50
|
+
thinsp: " ",
|
|
51
|
+
trade: "™"
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* `String.fromCodePoint` raises on out-of-range / non-finite inputs; the
|
|
55
|
+
* agent's untrusted-payload assumption means we can't let a single broken
|
|
56
|
+
* numeric reference crash the whole decode. Empty string on rejection is
|
|
57
|
+
* the safest outcome (matches what most browsers do for invalid refs).
|
|
58
|
+
*/
|
|
59
|
+
function safeFromCodePoint(cp) {
|
|
60
|
+
if (!Number.isFinite(cp) || cp < 0 || cp > 1114111) return "";
|
|
61
|
+
try {
|
|
62
|
+
return String.fromCodePoint(cp);
|
|
63
|
+
} catch {
|
|
64
|
+
return "";
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Decode numeric character references (`&#x...;` / `&#NNN;`) and the curated
|
|
69
|
+
* subset of named entities in {@link NAMED_ENTITIES}. Unknown named entities
|
|
70
|
+
* are left as-is so the model can detect them rather than seeing a silently
|
|
71
|
+
* corrupted token.
|
|
72
|
+
*
|
|
73
|
+
* `&` is decoded LAST so a double-escaped string like `&lt;` resolves
|
|
74
|
+
* to `<` (not `<`) — that's the spec-correct single-pass decode shape.
|
|
75
|
+
*/
|
|
76
|
+
function decodeHtmlEntities(text) {
|
|
77
|
+
let s = text.replace(/&#x([0-9a-f]+);/gi, (_, h) => safeFromCodePoint(Number.parseInt(h, 16))).replace(/&#(\d+);/g, (_, n) => safeFromCodePoint(Number.parseInt(n, 10)));
|
|
78
|
+
s = s.replace(/&([a-z][a-z0-9]*);/gi, (full, name) => {
|
|
79
|
+
const key = name.toLowerCase();
|
|
80
|
+
if (key === "amp") return full;
|
|
81
|
+
return NAMED_ENTITIES[key] ?? full;
|
|
82
|
+
});
|
|
83
|
+
return s.replace(/&/gi, "&");
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Strip `<script>`, `<style>`, `<noscript>`, comments, and every other tag.
|
|
87
|
+
* Block-level openers (`<br>`, `<p>`, `<div>`, `<li>`, `<tr>`, `<h1-6>`) are
|
|
88
|
+
* replaced with newlines so the resulting plaintext preserves readable
|
|
89
|
+
* structure. Other tags collapse to spaces.
|
|
90
|
+
*/
|
|
91
|
+
function stripHtml(html) {
|
|
92
|
+
return html.replace(/<script[\s\S]*?<\/script>/gi, " ").replace(/<style[\s\S]*?<\/style>/gi, " ").replace(/<noscript[\s\S]*?<\/noscript>/gi, " ").replace(/<!--[\s\S]*?-->/g, " ").replace(/<(?:br|p|div|li|tr|h[1-6])[^>]*>/gi, "\n").replace(/<[^>]+>/g, " ");
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Inline variant — strip tags + decode entities + collapse all whitespace
|
|
96
|
+
* (including line breaks) into single spaces. For short snippets where
|
|
97
|
+
* structural breaks would be visual noise (search-result titles, link text).
|
|
98
|
+
*/
|
|
99
|
+
function stripHtmlInline(html) {
|
|
100
|
+
return decodeHtmlEntities(stripHtml(html)).replace(/\s+/g, " ").trim();
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Block variant — strip tags + decode entities, preserve newlines emitted
|
|
104
|
+
* by block-level openers, then collapse inline whitespace per-line and drop
|
|
105
|
+
* empty lines. For full-page reductions where structure aids reading.
|
|
106
|
+
*/
|
|
107
|
+
function stripHtmlBlock(html) {
|
|
108
|
+
return decodeHtmlEntities(stripHtml(html)).split("\n").map((line) => line.replace(/[ \t]+/g, " ").trim()).filter((line) => line.length > 0).join("\n");
|
|
109
|
+
}
|
|
110
|
+
//#endregion
|
|
111
|
+
//#region src/tools/fetch-url.ts
|
|
112
|
+
/**
|
|
113
|
+
* Fetch a URL and return its text content.
|
|
114
|
+
*
|
|
115
|
+
* Companion to `web_search` (when registered) — after the model finds a
|
|
116
|
+
* promising result it fetches the page to read the body in full. HTML is
|
|
117
|
+
* reduced to plain text because the model rarely benefits from the markup
|
|
118
|
+
* and we want to keep the payload inside the per-turn output budget.
|
|
119
|
+
*
|
|
120
|
+
* Restrictions:
|
|
121
|
+
* - Only http(s) URLs. Anything else (file://, data:, ftp:) is rejected so
|
|
122
|
+
* the tool can't be coerced into reading local files or arbitrary
|
|
123
|
+
* resources.
|
|
124
|
+
* - SSRF guard with TOCTOU defense: the host is DNS-resolved against the
|
|
125
|
+
* loopback / link-local / private / reserved blocklist BEFORE the
|
|
126
|
+
* request, and the resolved IP is then PINNED at the connection layer
|
|
127
|
+
* via `node:http(s).request({ host: <resolved IP>, headers: { Host: ... },
|
|
128
|
+
* servername: ... })`. This closes the classic DNS-rebinding gap where a
|
|
129
|
+
* `globalThis.fetch` re-resolves the hostname and connects to a freshly
|
|
130
|
+
* minted private IP. TLS verification still runs against the original
|
|
131
|
+
* hostname via the explicit `servername` option.
|
|
132
|
+
*
|
|
133
|
+
* Note: the more idiomatic Node fix (an `undici.Agent` with a custom
|
|
134
|
+
* `connect.lookup`) is silently a no-op on Bun ≤1.3 — both
|
|
135
|
+
* `undici.Agent` and `node:https.Agent.lookup` are stubbed out. The
|
|
136
|
+
* explicit `host: IP` + `Host:` header dance is the only mechanism that
|
|
137
|
+
* actually pins on the current Bun runtime.
|
|
138
|
+
* - Redirects are followed manually (up to `MAX_REDIRECTS`) and each hop
|
|
139
|
+
* is re-validated, so a public hostname that 302s into a metadata
|
|
140
|
+
* endpoint is rejected at the redirect step.
|
|
141
|
+
* - 15s hard timeout per request.
|
|
142
|
+
* - Output capped at `max_bytes` (default 200 KiB) — long pages are
|
|
143
|
+
* truncated with an explicit marker. The pinned reader also stops
|
|
144
|
+
* buffering past 2× the cap so a multi-MB body can't OOM us.
|
|
145
|
+
*/
|
|
146
|
+
const DEFAULT_MAX_BYTES = 200 * 1024;
|
|
147
|
+
const HARD_MAX_BYTES = 1024 * 1024;
|
|
148
|
+
const HTTP_TIMEOUT_MS = 15e3;
|
|
149
|
+
const MAX_REDIRECTS = 5;
|
|
150
|
+
/**
|
|
151
|
+
* IPv4 ranges that the SSRF guard refuses to fetch from. Covers the cloud
|
|
152
|
+
* metadata services (AWS/Azure 169.254.169.254, GCP routes through the same
|
|
153
|
+
* link-local block) plus every RFC1918 / loopback / reserved block a model
|
|
154
|
+
* could be prompt-injected into hitting.
|
|
155
|
+
*/
|
|
156
|
+
function isBlockedIPv4(ip) {
|
|
157
|
+
const parts = ip.split(".").map(Number);
|
|
158
|
+
if (parts.length !== 4 || parts.some((p) => !Number.isInteger(p) || p < 0 || p > 255)) return true;
|
|
159
|
+
const [a, b] = parts;
|
|
160
|
+
if (a === 0) return true;
|
|
161
|
+
if (a === 10) return true;
|
|
162
|
+
if (a === 127) return true;
|
|
163
|
+
if (a === 169 && b === 254) return true;
|
|
164
|
+
if (a === 172 && b >= 16 && b <= 31) return true;
|
|
165
|
+
if (a === 192 && b === 168) return true;
|
|
166
|
+
if (a === 192 && b === 0 && parts[2] === 0) return true;
|
|
167
|
+
if (a === 198 && (b === 18 || b === 19)) return true;
|
|
168
|
+
if (a >= 224) return true;
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
function isBlockedIPv6(ip) {
|
|
172
|
+
const lower = ip.toLowerCase();
|
|
173
|
+
const v4MappedDotted = lower.match(/^::ffff:([0-9.]+)$/);
|
|
174
|
+
if (v4MappedDotted) return isBlockedIPv4(v4MappedDotted[1]);
|
|
175
|
+
const v4MappedHex = lower.match(/^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/);
|
|
176
|
+
if (v4MappedHex) {
|
|
177
|
+
const hi = Number.parseInt(v4MappedHex[1], 16);
|
|
178
|
+
const lo = Number.parseInt(v4MappedHex[2], 16);
|
|
179
|
+
return isBlockedIPv4(`${hi >> 8 & 255}.${hi & 255}.${lo >> 8 & 255}.${lo & 255}`);
|
|
180
|
+
}
|
|
181
|
+
if (lower === "::" || lower === "::1") return true;
|
|
182
|
+
if (/^f[cd][0-9a-f]{0,2}:/.test(lower)) return true;
|
|
183
|
+
if (/^fe[89ab][0-9a-f]?:/.test(lower)) return true;
|
|
184
|
+
if (lower.startsWith("ff")) return true;
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
function isBlockedAddress(ip) {
|
|
188
|
+
const family = isIP(ip);
|
|
189
|
+
if (family === 4) return isBlockedIPv4(ip);
|
|
190
|
+
if (family === 6) return isBlockedIPv6(ip);
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Normalize a hostname for allowlist comparison: lowercase, strip a trailing
|
|
195
|
+
* dot (FQDN root) and IPv6 brackets, drop a leading dot on allowlist entries.
|
|
196
|
+
*/
|
|
197
|
+
function normalizeHost(host) {
|
|
198
|
+
let h = host.trim().toLowerCase();
|
|
199
|
+
if (h.startsWith("[") && h.endsWith("]")) h = h.slice(1, -1);
|
|
200
|
+
if (h.endsWith(".")) h = h.slice(0, -1);
|
|
201
|
+
if (h.startsWith(".")) h = h.slice(1);
|
|
202
|
+
return h;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Site identity for cross-host redirect detection: the lowercased hostname
|
|
206
|
+
* with a leading `www.` stripped, so `example.com` ⇄ `www.example.com` and a
|
|
207
|
+
* path-only redirect aren't flagged as cross-host. Returns `null` for an
|
|
208
|
+
* unparseable URL (caller skips the note rather than guessing).
|
|
209
|
+
*/
|
|
210
|
+
function sameSiteHost(url) {
|
|
211
|
+
try {
|
|
212
|
+
return new URL(url).hostname.toLowerCase().replace(/^www\./, "");
|
|
213
|
+
} catch {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Host-suffix allowlist check. An empty / undefined list means "no allowlist"
|
|
219
|
+
* → every host passes (the SSRF blocklist is the only gate). Otherwise the
|
|
220
|
+
* host must equal, or be a subdomain of, one of the entries. Subdomain match
|
|
221
|
+
* is on a dot boundary so `example.com` matches `docs.example.com` but not
|
|
222
|
+
* `notexample.com`.
|
|
223
|
+
*/
|
|
224
|
+
function isHostAllowed(hostname, allowHosts) {
|
|
225
|
+
if (!allowHosts || allowHosts.length === 0) return true;
|
|
226
|
+
const host = normalizeHost(hostname);
|
|
227
|
+
for (const entry of allowHosts) {
|
|
228
|
+
const allowed = normalizeHost(entry);
|
|
229
|
+
if (allowed.length === 0) continue;
|
|
230
|
+
if (host === allowed || host.endsWith(`.${allowed}`)) return true;
|
|
231
|
+
}
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Resolve `hostname` and refuse if any answer falls in a blocked range.
|
|
236
|
+
*
|
|
237
|
+
* Returns the first allowed `{ address, family }` so the caller can pin
|
|
238
|
+
* the connection to it. Throws on any blocked answer so a multi-record
|
|
239
|
+
* response with one private record fails closed.
|
|
240
|
+
*/
|
|
241
|
+
async function resolveAndCheck(hostname, allowHosts) {
|
|
242
|
+
if (!isHostAllowed(hostname, allowHosts)) throw new Error(`refused: ${hostname} is not in the configured egress allowlist`);
|
|
243
|
+
const bare = hostname.startsWith("[") && hostname.endsWith("]") ? hostname.slice(1, -1) : hostname;
|
|
244
|
+
const family = isIP(bare);
|
|
245
|
+
if (family === 4 || family === 6) {
|
|
246
|
+
if (isBlockedAddress(bare)) throw new Error(`refused: ${bare} is in a blocked range`);
|
|
247
|
+
return {
|
|
248
|
+
address: bare,
|
|
249
|
+
family
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
const answers = await promises.lookup(bare, {
|
|
253
|
+
all: true,
|
|
254
|
+
verbatim: true
|
|
255
|
+
});
|
|
256
|
+
if (answers.length === 0) throw new Error(`DNS lookup returned no records for ${bare}`);
|
|
257
|
+
for (const { address } of answers) if (isBlockedAddress(address)) throw new Error(`refused: ${bare} resolves to blocked address ${address}`);
|
|
258
|
+
const first = answers[0];
|
|
259
|
+
const fam = first.family === 6 ? 6 : 4;
|
|
260
|
+
return {
|
|
261
|
+
address: first.address,
|
|
262
|
+
family: fam
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
function buildHostHeader(url) {
|
|
266
|
+
if (!url.port) return url.hostname;
|
|
267
|
+
const defaultPort = url.protocol === "https:" ? "443" : "80";
|
|
268
|
+
return url.port === defaultPort ? url.hostname : `${url.hostname}:${url.port}`;
|
|
269
|
+
}
|
|
270
|
+
function pinnedRequest(url, pinnedIp, family, headers, signal, maxBufferBytes) {
|
|
271
|
+
return new Promise((resolve, reject) => {
|
|
272
|
+
const isHttps = url.protocol === "https:";
|
|
273
|
+
const options = {
|
|
274
|
+
host: pinnedIp,
|
|
275
|
+
port: url.port ? Number.parseInt(url.port, 10) : isHttps ? 443 : 80,
|
|
276
|
+
path: `${url.pathname}${url.search}`,
|
|
277
|
+
method: "GET",
|
|
278
|
+
family,
|
|
279
|
+
headers: {
|
|
280
|
+
...headers,
|
|
281
|
+
Host: buildHostHeader(url)
|
|
282
|
+
},
|
|
283
|
+
...isHttps ? { servername: url.hostname } : {}
|
|
284
|
+
};
|
|
285
|
+
const request$2 = isHttps ? request$1 : request;
|
|
286
|
+
const chunks = [];
|
|
287
|
+
let received = 0;
|
|
288
|
+
let settled = false;
|
|
289
|
+
let req;
|
|
290
|
+
let timer;
|
|
291
|
+
const teardown = () => {
|
|
292
|
+
try {
|
|
293
|
+
req?.destroy();
|
|
294
|
+
} catch {}
|
|
295
|
+
};
|
|
296
|
+
const finish = (settler) => {
|
|
297
|
+
if (settled) return;
|
|
298
|
+
settled = true;
|
|
299
|
+
if (timer) clearTimeout(timer);
|
|
300
|
+
signal.removeEventListener("abort", onSignalAbort);
|
|
301
|
+
settler();
|
|
302
|
+
};
|
|
303
|
+
function onSignalAbort() {
|
|
304
|
+
finish(() => reject(/* @__PURE__ */ new Error("request aborted")));
|
|
305
|
+
teardown();
|
|
306
|
+
}
|
|
307
|
+
if (signal.aborted) {
|
|
308
|
+
reject(/* @__PURE__ */ new Error("request aborted"));
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
timer = setTimeout(() => {
|
|
312
|
+
finish(() => reject(/* @__PURE__ */ new Error(`request timed out after ${HTTP_TIMEOUT_MS}ms`)));
|
|
313
|
+
teardown();
|
|
314
|
+
}, HTTP_TIMEOUT_MS);
|
|
315
|
+
signal.addEventListener("abort", onSignalAbort, { once: true });
|
|
316
|
+
req = request$2(options, (res) => {
|
|
317
|
+
const status = res.statusCode ?? 0;
|
|
318
|
+
const headersOut = res.headers;
|
|
319
|
+
const contentType = (headersOut["content-type"] ?? "").toString().toLowerCase();
|
|
320
|
+
const location = typeof headersOut.location === "string" ? headersOut.location : null;
|
|
321
|
+
if (status >= 300 && status < 400 && location) {
|
|
322
|
+
res.resume();
|
|
323
|
+
finish(() => resolve({
|
|
324
|
+
status,
|
|
325
|
+
url: url.toString(),
|
|
326
|
+
contentType,
|
|
327
|
+
body: "",
|
|
328
|
+
location
|
|
329
|
+
}));
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
res.on("data", (chunk) => {
|
|
333
|
+
if (received >= maxBufferBytes) {
|
|
334
|
+
teardown();
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
received += chunk.length;
|
|
338
|
+
chunks.push(chunk);
|
|
339
|
+
});
|
|
340
|
+
res.on("end", () => {
|
|
341
|
+
const body = Buffer.concat(chunks).toString("utf-8");
|
|
342
|
+
finish(() => resolve({
|
|
343
|
+
status,
|
|
344
|
+
url: url.toString(),
|
|
345
|
+
contentType,
|
|
346
|
+
body,
|
|
347
|
+
location: null
|
|
348
|
+
}));
|
|
349
|
+
});
|
|
350
|
+
res.on("close", () => {
|
|
351
|
+
if (!settled) {
|
|
352
|
+
const body = Buffer.concat(chunks).toString("utf-8");
|
|
353
|
+
finish(() => resolve({
|
|
354
|
+
status,
|
|
355
|
+
url: url.toString(),
|
|
356
|
+
contentType,
|
|
357
|
+
body,
|
|
358
|
+
location: null
|
|
359
|
+
}));
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
res.on("error", (err) => finish(() => reject(err)));
|
|
363
|
+
});
|
|
364
|
+
req.on("error", (err) => finish(() => reject(err)));
|
|
365
|
+
req.end();
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
async function fetchWithSsrfGuard(startUrl, headers, signal, maxBufferBytes, deps = {}, allowHosts) {
|
|
369
|
+
const resolver = deps.resolver ?? ((hostname) => resolveAndCheck(hostname, allowHosts));
|
|
370
|
+
const requestImpl = deps.requestImpl ?? pinnedRequest;
|
|
371
|
+
let current = startUrl;
|
|
372
|
+
for (let hop = 0; hop <= MAX_REDIRECTS; hop++) {
|
|
373
|
+
const { address, family } = await resolver(current.hostname);
|
|
374
|
+
const res = await requestImpl(current, address, family, headers, signal, maxBufferBytes);
|
|
375
|
+
if (!res.location) return res;
|
|
376
|
+
if (hop === MAX_REDIRECTS) throw new Error(`too many redirects (>${MAX_REDIRECTS})`);
|
|
377
|
+
let next;
|
|
378
|
+
try {
|
|
379
|
+
next = new URL(res.location, current);
|
|
380
|
+
} catch {
|
|
381
|
+
throw new Error(`invalid redirect target: ${res.location}`);
|
|
382
|
+
}
|
|
383
|
+
if (next.protocol !== "http:" && next.protocol !== "https:") throw new Error(`refused redirect to non-http(s) target: ${next.protocol}`);
|
|
384
|
+
current = next;
|
|
385
|
+
}
|
|
386
|
+
throw new Error("redirect loop exited unexpectedly");
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Opt-in, process-local response cache for `fetch_url`. Disabled unless
|
|
390
|
+
* `behavior.fetchUrlCacheTtlMs` is set (> 0). Bounded by entry count and total
|
|
391
|
+
* cached bytes; eviction is oldest-first (insertion order via `Map`, which is
|
|
392
|
+
* a good-enough LRU because we delete-then-set on hit to refresh recency).
|
|
393
|
+
*
|
|
394
|
+
* Keyed on `<url>\u0000<cap>` so two calls with different `max_bytes` (hence
|
|
395
|
+
* different truncation) don't alias. Only successful text responses are stored
|
|
396
|
+
* — the caller gates on status before writing.
|
|
397
|
+
*/
|
|
398
|
+
const FETCH_CACHE_MAX_ENTRIES = 64;
|
|
399
|
+
const FETCH_CACHE_MAX_BYTES = 16 * 1024 * 1024;
|
|
400
|
+
const fetchCache = /* @__PURE__ */ new Map();
|
|
401
|
+
let fetchCacheBytes = 0;
|
|
402
|
+
function cacheKey(url, cap) {
|
|
403
|
+
return `${url}\u0000${cap}`;
|
|
404
|
+
}
|
|
405
|
+
function cacheGet(key) {
|
|
406
|
+
const entry = fetchCache.get(key);
|
|
407
|
+
if (!entry) return null;
|
|
408
|
+
if (Date.now() >= entry.expiresAt) {
|
|
409
|
+
fetchCache.delete(key);
|
|
410
|
+
fetchCacheBytes -= entry.bytes;
|
|
411
|
+
return null;
|
|
412
|
+
}
|
|
413
|
+
fetchCache.delete(key);
|
|
414
|
+
fetchCache.set(key, entry);
|
|
415
|
+
return entry.output;
|
|
416
|
+
}
|
|
417
|
+
function cacheSet(key, output, ttlMs) {
|
|
418
|
+
const bytes = Buffer.byteLength(output, "utf-8");
|
|
419
|
+
if (bytes > FETCH_CACHE_MAX_BYTES) return;
|
|
420
|
+
const prev = fetchCache.get(key);
|
|
421
|
+
if (prev) {
|
|
422
|
+
fetchCache.delete(key);
|
|
423
|
+
fetchCacheBytes -= prev.bytes;
|
|
424
|
+
}
|
|
425
|
+
fetchCache.set(key, {
|
|
426
|
+
output,
|
|
427
|
+
expiresAt: Date.now() + ttlMs,
|
|
428
|
+
bytes
|
|
429
|
+
});
|
|
430
|
+
fetchCacheBytes += bytes;
|
|
431
|
+
while (fetchCache.size > FETCH_CACHE_MAX_ENTRIES || fetchCacheBytes > FETCH_CACHE_MAX_BYTES) {
|
|
432
|
+
const oldest = fetchCache.keys().next().value;
|
|
433
|
+
if (oldest === void 0) break;
|
|
434
|
+
const victim = fetchCache.get(oldest);
|
|
435
|
+
fetchCache.delete(oldest);
|
|
436
|
+
fetchCacheBytes -= victim.bytes;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
/** Test seam — drop all cached entries. */
|
|
440
|
+
function clearFetchUrlCache() {
|
|
441
|
+
fetchCache.clear();
|
|
442
|
+
fetchCacheBytes = 0;
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* Test seam — exercise the cache get/set/eviction in isolation without
|
|
446
|
+
* standing up the full SSRF + HTTP path. Not part of the public tool surface;
|
|
447
|
+
* `execute` uses the module-private functions directly.
|
|
448
|
+
*/
|
|
449
|
+
const __fetchCacheTestApi = {
|
|
450
|
+
key: cacheKey,
|
|
451
|
+
get: cacheGet,
|
|
452
|
+
set: cacheSet,
|
|
453
|
+
size: () => fetchCache.size,
|
|
454
|
+
bytes: () => fetchCacheBytes
|
|
455
|
+
};
|
|
456
|
+
const fetchUrl = {
|
|
457
|
+
isConcurrencySafe: true,
|
|
458
|
+
spec: {
|
|
459
|
+
name: "fetch_url",
|
|
460
|
+
description: "Fetch a public URL and return its body as plain text. HTML is reduced to text (scripts, styles, tags stripped; entities decoded). If a `web_search` tool is also available, use it first to find the URL. Only http(s) URLs are accepted; loopback, link-local, private, and cloud-metadata addresses are refused. Output is capped (default 200 KiB).",
|
|
461
|
+
inputSchema: {
|
|
462
|
+
type: "object",
|
|
463
|
+
properties: {
|
|
464
|
+
url: {
|
|
465
|
+
type: "string",
|
|
466
|
+
description: "HTTP or HTTPS URL to fetch."
|
|
467
|
+
},
|
|
468
|
+
max_bytes: {
|
|
469
|
+
type: "number",
|
|
470
|
+
description: `Truncate the response body beyond this many bytes. Default: ${DEFAULT_MAX_BYTES}. Hard cap: ${HARD_MAX_BYTES}.`
|
|
471
|
+
}
|
|
472
|
+
},
|
|
473
|
+
required: ["url"]
|
|
474
|
+
}
|
|
475
|
+
},
|
|
476
|
+
async execute({ url, max_bytes }, ctx) {
|
|
477
|
+
const raw = typeof url === "string" ? url.trim() : "";
|
|
478
|
+
if (!raw) return "fetch_url error: `url` is required";
|
|
479
|
+
let parsed;
|
|
480
|
+
try {
|
|
481
|
+
parsed = new URL(raw);
|
|
482
|
+
} catch {
|
|
483
|
+
return `fetch_url error: invalid URL: ${raw}`;
|
|
484
|
+
}
|
|
485
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return `fetch_url error: only http(s) URLs are allowed (got ${parsed.protocol})`;
|
|
486
|
+
const cap = Math.min(typeof max_bytes === "number" && max_bytes > 0 ? Math.floor(max_bytes) : DEFAULT_MAX_BYTES, HARD_MAX_BYTES);
|
|
487
|
+
const bufferCap = Math.min(cap * 2, HARD_MAX_BYTES * 2);
|
|
488
|
+
const ttlMs = ctx.behavior?.fetchUrlCacheTtlMs;
|
|
489
|
+
const cacheEnabled = typeof ttlMs === "number" && ttlMs > 0;
|
|
490
|
+
const key = cacheKey(parsed.toString(), cap);
|
|
491
|
+
if (cacheEnabled) {
|
|
492
|
+
const hit = cacheGet(key);
|
|
493
|
+
if (hit !== null) return hit;
|
|
494
|
+
}
|
|
495
|
+
try {
|
|
496
|
+
const res = await fetchWithSsrfGuard(parsed, {
|
|
497
|
+
"user-agent": "Mozilla/5.0 (compatible; zidane/1.0)",
|
|
498
|
+
"accept": "text/html,text/plain,application/json;q=0.9,*/*;q=0.5",
|
|
499
|
+
"accept-encoding": "identity"
|
|
500
|
+
}, ctx.signal, bufferCap, {}, ctx.behavior?.fetchUrlAllowHosts);
|
|
501
|
+
const body = res.body;
|
|
502
|
+
const text = res.contentType.includes("html") || /^\s*<!doctype html|^\s*<html/i.test(body) ? stripHtmlBlock(body) : body;
|
|
503
|
+
const out = text.length > cap ? `${text.slice(0, cap)}\n\n[truncated at ${cap} bytes; full length ${text.length}]` : text;
|
|
504
|
+
const finalHost = sameSiteHost(res.url);
|
|
505
|
+
const requestedHost = sameSiteHost(parsed.toString());
|
|
506
|
+
const redirectNote = finalHost && requestedHost && finalHost !== requestedHost ? `Note: ${parsed.hostname} redirected to a different host (${new URL(res.url).hostname}).\n` : "";
|
|
507
|
+
const formatted = `URL: ${res.url}\nStatus: ${res.status}\nContent-Type: ${res.contentType || "(none)"}\n${redirectNote}\n` + out;
|
|
508
|
+
if (cacheEnabled && res.status >= 200 && res.status < 300) cacheSet(key, formatted, ttlMs);
|
|
509
|
+
return formatted;
|
|
510
|
+
} catch (err) {
|
|
511
|
+
return `fetch_url error: ${errorMessage(err)}`;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
};
|
|
515
|
+
//#endregion
|
|
516
|
+
export { isBlockedAddress as a, stripHtmlInline as c, fetchWithSsrfGuard as i, clearFetchUrlCache as n, isHostAllowed as o, fetchUrl as r, pinnedRequest as s, __fetchCacheTestApi as t };
|
|
517
|
+
|
|
518
|
+
//# sourceMappingURL=fetch-url-CPxfiXDa.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fetch-url-CPxfiXDa.js","names":["dns","request","httpsRequest","httpRequest"],"sources":["../src/tools/_html.ts","../src/tools/fetch-url.ts"],"sourcesContent":["/**\n * Shared HTML → plain-text helpers used by the web-egress tools (`web_search`\n * snippet extraction, `fetch_url` body decoding).\n *\n * Best-effort by contract: a real HTML parser would be more correct but the\n * payload is always model context, never user-facing rendered output, so a\n * tag-stripper plus a curated entity table covers the practical cases\n * (Wikipedia, GitHub READMEs, MDN docs, Stack Overflow answers). When a tool\n * needs richer parsing it should reach for an actual parser rather than\n * extending these helpers ad-hoc.\n */\n\n/**\n * Curated named-entity table. Kept narrow on purpose — the long tail\n * (`Å`, `×`, mathematical refs, …) is out of scope because the\n * model handles unknown entities fine when they pass through verbatim. Add\n * entries when an entity is observed mangling real output, not speculatively.\n *\n * Keys are stored without the leading `&` / trailing `;` and matched\n * case-insensitively by the decoder.\n */\nconst NAMED_ENTITIES: Record<string, string> = {\n amp: '&',\n apos: '\\'',\n bull: '•',\n copy: '\\u00A9',\n ensp: ' ',\n emsp: ' ',\n gt: '>',\n hellip: '\\u2026',\n laquo: '\\u00AB',\n ldquo: '\\u201C',\n lsquo: '\\u2018',\n lt: '<',\n mdash: '\\u2014',\n middot: '\\u00B7',\n nbsp: ' ',\n ndash: '\\u2013',\n quot: '\"',\n raquo: '\\u00BB',\n rdquo: '\\u201D',\n reg: '\\u00AE',\n rsquo: '\\u2019',\n thinsp: ' ',\n trade: '\\u2122',\n}\n\n/**\n * `String.fromCodePoint` raises on out-of-range / non-finite inputs; the\n * agent's untrusted-payload assumption means we can't let a single broken\n * numeric reference crash the whole decode. Empty string on rejection is\n * the safest outcome (matches what most browsers do for invalid refs).\n */\nfunction safeFromCodePoint(cp: number): string {\n if (!Number.isFinite(cp) || cp < 0 || cp > 0x10FFFF)\n return ''\n try {\n return String.fromCodePoint(cp)\n }\n catch {\n return ''\n }\n}\n\n/**\n * Decode numeric character references (`&#x...;` / `&#NNN;`) and the curated\n * subset of named entities in {@link NAMED_ENTITIES}. Unknown named entities\n * are left as-is so the model can detect them rather than seeing a silently\n * corrupted token.\n *\n * `&` is decoded LAST so a double-escaped string like `&lt;` resolves\n * to `<` (not `<`) — that's the spec-correct single-pass decode shape.\n */\nexport function decodeHtmlEntities(text: string): string {\n let s = text\n .replace(/&#x([0-9a-f]+);/gi, (_, h) => safeFromCodePoint(Number.parseInt(h, 16)))\n .replace(/&#(\\d+);/g, (_, n) => safeFromCodePoint(Number.parseInt(n, 10)))\n s = s.replace(/&([a-z][a-z0-9]*);/gi, (full, name) => {\n const key = name.toLowerCase()\n // Defer `amp` so `&lt;` doesn't double-decode in a single pass.\n if (key === 'amp')\n return full\n const replacement = NAMED_ENTITIES[key]\n return replacement ?? full\n })\n return s.replace(/&/gi, '&')\n}\n\n/**\n * Strip `<script>`, `<style>`, `<noscript>`, comments, and every other tag.\n * Block-level openers (`<br>`, `<p>`, `<div>`, `<li>`, `<tr>`, `<h1-6>`) are\n * replaced with newlines so the resulting plaintext preserves readable\n * structure. Other tags collapse to spaces.\n */\nexport function stripHtml(html: string): string {\n return html\n .replace(/<script[\\s\\S]*?<\\/script>/gi, ' ')\n .replace(/<style[\\s\\S]*?<\\/style>/gi, ' ')\n .replace(/<noscript[\\s\\S]*?<\\/noscript>/gi, ' ')\n .replace(/<!--[\\s\\S]*?-->/g, ' ')\n .replace(/<(?:br|p|div|li|tr|h[1-6])[^>]*>/gi, '\\n')\n .replace(/<[^>]+>/g, ' ')\n}\n\n/**\n * Inline variant — strip tags + decode entities + collapse all whitespace\n * (including line breaks) into single spaces. For short snippets where\n * structural breaks would be visual noise (search-result titles, link text).\n */\nexport function stripHtmlInline(html: string): string {\n return decodeHtmlEntities(stripHtml(html))\n .replace(/\\s+/g, ' ')\n .trim()\n}\n\n/**\n * Block variant — strip tags + decode entities, preserve newlines emitted\n * by block-level openers, then collapse inline whitespace per-line and drop\n * empty lines. For full-page reductions where structure aids reading.\n */\nexport function stripHtmlBlock(html: string): string {\n return decodeHtmlEntities(stripHtml(html))\n .split('\\n')\n .map(line => line.replace(/[ \\t]+/g, ' ').trim())\n .filter(line => line.length > 0)\n .join('\\n')\n}\n","import type { RequestOptions as HttpRequestOptions } from 'node:http'\nimport type { RequestOptions as HttpsRequestOptions } from 'node:https'\nimport type { ToolContext, ToolDef } from './types'\nimport { Buffer } from 'node:buffer'\nimport { promises as dns } from 'node:dns'\nimport { request as httpRequest } from 'node:http'\nimport { request as httpsRequest } from 'node:https'\nimport { isIP } from 'node:net'\nimport { errorMessage } from '../errors'\nimport { stripHtmlBlock } from './_html'\n\n/**\n * Fetch a URL and return its text content.\n *\n * Companion to `web_search` (when registered) — after the model finds a\n * promising result it fetches the page to read the body in full. HTML is\n * reduced to plain text because the model rarely benefits from the markup\n * and we want to keep the payload inside the per-turn output budget.\n *\n * Restrictions:\n * - Only http(s) URLs. Anything else (file://, data:, ftp:) is rejected so\n * the tool can't be coerced into reading local files or arbitrary\n * resources.\n * - SSRF guard with TOCTOU defense: the host is DNS-resolved against the\n * loopback / link-local / private / reserved blocklist BEFORE the\n * request, and the resolved IP is then PINNED at the connection layer\n * via `node:http(s).request({ host: <resolved IP>, headers: { Host: ... },\n * servername: ... })`. This closes the classic DNS-rebinding gap where a\n * `globalThis.fetch` re-resolves the hostname and connects to a freshly\n * minted private IP. TLS verification still runs against the original\n * hostname via the explicit `servername` option.\n *\n * Note: the more idiomatic Node fix (an `undici.Agent` with a custom\n * `connect.lookup`) is silently a no-op on Bun ≤1.3 — both\n * `undici.Agent` and `node:https.Agent.lookup` are stubbed out. The\n * explicit `host: IP` + `Host:` header dance is the only mechanism that\n * actually pins on the current Bun runtime.\n * - Redirects are followed manually (up to `MAX_REDIRECTS`) and each hop\n * is re-validated, so a public hostname that 302s into a metadata\n * endpoint is rejected at the redirect step.\n * - 15s hard timeout per request.\n * - Output capped at `max_bytes` (default 200 KiB) — long pages are\n * truncated with an explicit marker. The pinned reader also stops\n * buffering past 2× the cap so a multi-MB body can't OOM us.\n */\n\nconst DEFAULT_MAX_BYTES = 200 * 1024\nconst HARD_MAX_BYTES = 1024 * 1024\nconst HTTP_TIMEOUT_MS = 15_000\nconst MAX_REDIRECTS = 5\n\n/**\n * IPv4 ranges that the SSRF guard refuses to fetch from. Covers the cloud\n * metadata services (AWS/Azure 169.254.169.254, GCP routes through the same\n * link-local block) plus every RFC1918 / loopback / reserved block a model\n * could be prompt-injected into hitting.\n */\nfunction isBlockedIPv4(ip: string): boolean {\n const parts = ip.split('.').map(Number)\n if (parts.length !== 4 || parts.some(p => !Number.isInteger(p) || p < 0 || p > 255))\n return true\n const [a, b] = parts as [number, number, number, number]\n if (a === 0)\n return true // 0.0.0.0/8 unspecified / \"this network\"\n if (a === 10)\n return true // 10/8 private\n if (a === 127)\n return true // loopback\n if (a === 169 && b === 254)\n return true // link-local + cloud metadata\n if (a === 172 && b >= 16 && b <= 31)\n return true // 172.16/12 private\n if (a === 192 && b === 168)\n return true // 192.168/16 private\n if (a === 192 && b === 0 && parts[2] === 0)\n return true // 192.0.0/24 IETF\n if (a === 198 && (b === 18 || b === 19))\n return true // 198.18/15 benchmark\n if (a >= 224)\n return true // multicast + reserved (224/4, 240/4)\n return false\n}\n\nfunction isBlockedIPv6(ip: string): boolean {\n const lower = ip.toLowerCase()\n // Normalise IPv4-mapped (::ffff:1.2.3.4) — recurse on the v4 tail. URL\n // parsing tends to canonicalise the dotted form into the hex pair\n // `::ffff:a9fe:a9fe`, so accept both shapes.\n const v4MappedDotted = lower.match(/^::ffff:([0-9.]+)$/)\n if (v4MappedDotted)\n return isBlockedIPv4(v4MappedDotted[1]!)\n const v4MappedHex = lower.match(/^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/)\n if (v4MappedHex) {\n const hi = Number.parseInt(v4MappedHex[1]!, 16)\n const lo = Number.parseInt(v4MappedHex[2]!, 16)\n const ipv4 = `${(hi >> 8) & 0xFF}.${hi & 0xFF}.${(lo >> 8) & 0xFF}.${lo & 0xFF}`\n return isBlockedIPv4(ipv4)\n }\n if (lower === '::' || lower === '::1')\n return true\n // fc00::/7 unique-local (covers fc.. and fd..)\n if (/^f[cd][0-9a-f]{0,2}:/.test(lower))\n return true\n // fe80::/10 link-local\n if (/^fe[89ab][0-9a-f]?:/.test(lower))\n return true\n // ff00::/8 multicast\n if (lower.startsWith('ff'))\n return true\n return false\n}\n\nexport function isBlockedAddress(ip: string): boolean {\n const family = isIP(ip)\n if (family === 4)\n return isBlockedIPv4(ip)\n if (family === 6)\n return isBlockedIPv6(ip)\n return true // not a recognised IP literal — refuse\n}\n\n/**\n * Normalize a hostname for allowlist comparison: lowercase, strip a trailing\n * dot (FQDN root) and IPv6 brackets, drop a leading dot on allowlist entries.\n */\nfunction normalizeHost(host: string): string {\n let h = host.trim().toLowerCase()\n if (h.startsWith('[') && h.endsWith(']'))\n h = h.slice(1, -1)\n if (h.endsWith('.'))\n h = h.slice(0, -1)\n if (h.startsWith('.'))\n h = h.slice(1)\n return h\n}\n\n/**\n * Site identity for cross-host redirect detection: the lowercased hostname\n * with a leading `www.` stripped, so `example.com` ⇄ `www.example.com` and a\n * path-only redirect aren't flagged as cross-host. Returns `null` for an\n * unparseable URL (caller skips the note rather than guessing).\n */\nfunction sameSiteHost(url: string): string | null {\n try {\n return new URL(url).hostname.toLowerCase().replace(/^www\\./, '')\n }\n catch {\n return null\n }\n}\n\n/**\n * Host-suffix allowlist check. An empty / undefined list means \"no allowlist\"\n * → every host passes (the SSRF blocklist is the only gate). Otherwise the\n * host must equal, or be a subdomain of, one of the entries. Subdomain match\n * is on a dot boundary so `example.com` matches `docs.example.com` but not\n * `notexample.com`.\n */\nexport function isHostAllowed(hostname: string, allowHosts: readonly string[] | undefined): boolean {\n if (!allowHosts || allowHosts.length === 0)\n return true\n const host = normalizeHost(hostname)\n for (const entry of allowHosts) {\n const allowed = normalizeHost(entry)\n if (allowed.length === 0)\n continue\n if (host === allowed || host.endsWith(`.${allowed}`))\n return true\n }\n return false\n}\n\n/**\n * Resolve `hostname` and refuse if any answer falls in a blocked range.\n *\n * Returns the first allowed `{ address, family }` so the caller can pin\n * the connection to it. Throws on any blocked answer so a multi-record\n * response with one private record fails closed.\n */\nasync function resolveAndCheck(hostname: string, allowHosts?: readonly string[]): Promise<{ address: string, family: 4 | 6 }> {\n // Egress allowlist (when configured) gates BEFORE DNS so a non-approved\n // host never even triggers a lookup. Enforced here so every redirect hop\n // is re-checked, not just the initial URL.\n if (!isHostAllowed(hostname, allowHosts))\n throw new Error(`refused: ${hostname} is not in the configured egress allowlist`)\n // `URL.hostname` keeps brackets around IPv6 literals (`[::1]`) — strip them\n // before the IP check so `isIP` recognises the literal and `dns.lookup`\n // doesn't try to resolve `[::1]` as a name.\n const bare = hostname.startsWith('[') && hostname.endsWith(']')\n ? hostname.slice(1, -1)\n : hostname\n const family = isIP(bare)\n if (family === 4 || family === 6) {\n if (isBlockedAddress(bare))\n throw new Error(`refused: ${bare} is in a blocked range`)\n return { address: bare, family }\n }\n const answers = await dns.lookup(bare, { all: true, verbatim: true })\n if (answers.length === 0)\n throw new Error(`DNS lookup returned no records for ${bare}`)\n for (const { address } of answers) {\n if (isBlockedAddress(address))\n throw new Error(`refused: ${bare} resolves to blocked address ${address}`)\n }\n const first = answers[0]!\n const fam = first.family === 6 ? 6 : 4\n return { address: first.address, family: fam }\n}\n\n/**\n * Pinned HTTP/HTTPS request. Connects to `pinnedIp` directly (bypassing\n * Node/Bun's own DNS resolution) while preserving the `Host:` header and\n * (for HTTPS) TLS SNI on the URL's original hostname. This is the\n * concrete defense against DNS rebinding — see the file-level docstring.\n *\n * Reads up to `maxBufferBytes` of body and then forcibly closes the\n * connection. The body is decoded as UTF-8; binary responses degrade to\n * mojibake but the tool's contract is text payloads anyway.\n */\ninterface PinnedResponse {\n status: number\n url: string\n contentType: string\n body: string\n location: string | null\n}\n\nfunction buildHostHeader(url: URL): string {\n if (!url.port)\n return url.hostname\n const defaultPort = url.protocol === 'https:' ? '443' : '80'\n return url.port === defaultPort ? url.hostname : `${url.hostname}:${url.port}`\n}\n\nexport function pinnedRequest(\n url: URL,\n pinnedIp: string,\n family: 4 | 6,\n headers: Record<string, string>,\n signal: AbortSignal,\n maxBufferBytes: number,\n): Promise<PinnedResponse> {\n return new Promise((resolve, reject) => {\n const isHttps = url.protocol === 'https:'\n const port = url.port\n ? Number.parseInt(url.port, 10)\n : (isHttps ? 443 : 80)\n\n // `host` is the literal IP, `servername` is the original hostname so\n // TLS cert verification still matches the cert's SAN/CN against the\n // user-visible name. Node uses `servername` for `checkServerIdentity`\n // automatically when set.\n const options: HttpRequestOptions & HttpsRequestOptions = {\n host: pinnedIp,\n port,\n path: `${url.pathname}${url.search}`,\n method: 'GET',\n family,\n headers: { ...headers, Host: buildHostHeader(url) },\n ...(isHttps ? { servername: url.hostname } : {}),\n }\n\n const request = isHttps ? httpsRequest : httpRequest\n const chunks: Buffer[] = []\n let received = 0\n // Single-shot settle guard. Bun's `node:http` is quirky around\n // `req.destroy(err)` — passing an error doesn't reliably surface as an\n // `error` event the way it does on Node. We track the settle state\n // ourselves and call `resolve` / `reject` directly from the abort /\n // timeout paths so the promise terminates even when the underlying\n // socket teardown is silent.\n let settled = false\n // Forward-declared so the `finish` / abort / timeout handlers can\n // close over `req` before it's assigned below. The request object is\n // wired into them via mutable refs because the handlers must be set\n // up *before* `request(options, callback)` fires its first event.\n let req: ReturnType<typeof httpRequest> | undefined\n let timer: ReturnType<typeof setTimeout> | undefined\n\n const teardown = (): void => {\n try { req?.destroy() }\n catch { /* tear-down best effort */ }\n }\n\n const finish = (settler: () => void): void => {\n if (settled)\n return\n settled = true\n if (timer)\n clearTimeout(timer)\n signal.removeEventListener('abort', onSignalAbort)\n settler()\n }\n\n function onSignalAbort(): void {\n finish(() => reject(new Error('request aborted')))\n teardown()\n }\n\n // Pre-aborted signal — short-circuit so we don't even open a socket.\n // (This is the realistic shape when a user hits Ctrl+C while the agent\n // is already shutting down: the signal is `.aborted` by the time the\n // tool function runs.)\n if (signal.aborted) {\n reject(new Error('request aborted'))\n return\n }\n\n timer = setTimeout(() => {\n finish(() => reject(new Error(`request timed out after ${HTTP_TIMEOUT_MS}ms`)))\n teardown()\n }, HTTP_TIMEOUT_MS)\n\n signal.addEventListener('abort', onSignalAbort, { once: true })\n\n req = request(options, (res) => {\n const status = res.statusCode ?? 0\n const headersOut = res.headers\n const contentType = (headersOut['content-type'] ?? '').toString().toLowerCase()\n const location = typeof headersOut.location === 'string' ? headersOut.location : null\n\n // Short-circuit on redirect responses — body is irrelevant.\n if (status >= 300 && status < 400 && location) {\n res.resume() // drain so the socket can be released\n finish(() => resolve({ status, url: url.toString(), contentType, body: '', location }))\n return\n }\n\n res.on('data', (chunk: Buffer) => {\n if (received >= maxBufferBytes) {\n // Stop accumulating; tear the socket down so the server stops\n // sending. We still resolve cleanly from `end` (or `close` if\n // the server doesn't finish flushing before the kill lands).\n teardown()\n return\n }\n received += chunk.length\n chunks.push(chunk)\n })\n res.on('end', () => {\n const body = Buffer.concat(chunks).toString('utf-8')\n finish(() => resolve({ status, url: url.toString(), contentType, body, location: null }))\n })\n res.on('close', () => {\n // Socket torn down before `end` (e.g. when our maxBufferBytes\n // trigger destroyed the request). Resolve with whatever we\n // buffered so far — the caller will treat it as truncated text.\n if (!settled) {\n const body = Buffer.concat(chunks).toString('utf-8')\n finish(() => resolve({ status, url: url.toString(), contentType, body, location: null }))\n }\n })\n res.on('error', err => finish(() => reject(err)))\n })\n\n req.on('error', (err: Error) => finish(() => reject(err)))\n req.end()\n })\n}\n\n/**\n * Walk up to {@link MAX_REDIRECTS} hops, validating the target host of each\n * 3xx response against the SSRF blocklist before following.\n *\n * `deps` is an internal seam for tests — production callers use the\n * defaults. Both knobs are intentionally opaque: hosts that want to\n * customize the blocklist should use a higher-level mechanism (a future\n * `behavior.fetchUrlAllowHosts` setting) rather than reaching into this.\n */\nexport interface FetchSsrfDeps {\n /** Resolve + validate a hostname. Defaults to {@link resolveAndCheck}. */\n resolver?: (hostname: string) => Promise<{ address: string, family: 4 | 6 }>\n /** Execute a single pinned HTTP request. Defaults to {@link pinnedRequest}. */\n requestImpl?: typeof pinnedRequest\n}\n\nexport async function fetchWithSsrfGuard(\n startUrl: URL,\n headers: Record<string, string>,\n signal: AbortSignal,\n maxBufferBytes: number,\n deps: FetchSsrfDeps = {},\n allowHosts?: readonly string[],\n): Promise<PinnedResponse> {\n // Default resolver binds the egress allowlist; a test-injected resolver\n // owns its own policy.\n const resolver = deps.resolver ?? ((hostname: string) => resolveAndCheck(hostname, allowHosts))\n const requestImpl = deps.requestImpl ?? pinnedRequest\n let current = startUrl\n for (let hop = 0; hop <= MAX_REDIRECTS; hop++) {\n const { address, family } = await resolver(current.hostname)\n const res = await requestImpl(current, address, family, headers, signal, maxBufferBytes)\n if (!res.location)\n return res\n if (hop === MAX_REDIRECTS)\n throw new Error(`too many redirects (>${MAX_REDIRECTS})`)\n let next: URL\n try {\n next = new URL(res.location, current)\n }\n catch {\n throw new Error(`invalid redirect target: ${res.location}`)\n }\n if (next.protocol !== 'http:' && next.protocol !== 'https:')\n throw new Error(`refused redirect to non-http(s) target: ${next.protocol}`)\n current = next\n }\n throw new Error('redirect loop exited unexpectedly')\n}\n\n/**\n * Opt-in, process-local response cache for `fetch_url`. Disabled unless\n * `behavior.fetchUrlCacheTtlMs` is set (> 0). Bounded by entry count and total\n * cached bytes; eviction is oldest-first (insertion order via `Map`, which is\n * a good-enough LRU because we delete-then-set on hit to refresh recency).\n *\n * Keyed on `<url>\\u0000<cap>` so two calls with different `max_bytes` (hence\n * different truncation) don't alias. Only successful text responses are stored\n * — the caller gates on status before writing.\n */\nconst FETCH_CACHE_MAX_ENTRIES = 64\nconst FETCH_CACHE_MAX_BYTES = 16 * 1024 * 1024 // 16 MiB total\n\ninterface FetchCacheEntry {\n /** Fully-formatted tool output (header + body) ready to return verbatim. */\n output: string\n expiresAt: number\n bytes: number\n}\n\nconst fetchCache = new Map<string, FetchCacheEntry>()\nlet fetchCacheBytes = 0\n\nfunction cacheKey(url: string, cap: number): string {\n return `${url}\\u0000${cap}`\n}\n\nfunction cacheGet(key: string): string | null {\n const entry = fetchCache.get(key)\n if (!entry)\n return null\n if (Date.now() >= entry.expiresAt) {\n fetchCache.delete(key)\n fetchCacheBytes -= entry.bytes\n return null\n }\n // Refresh recency: re-insert at the tail of the Map's iteration order.\n fetchCache.delete(key)\n fetchCache.set(key, entry)\n return entry.output\n}\n\nfunction cacheSet(key: string, output: string, ttlMs: number): void {\n const bytes = Buffer.byteLength(output, 'utf-8')\n // A single oversize payload that can't fit under the byte cap is simply not\n // cached (still returned to the caller) rather than evicting everything.\n if (bytes > FETCH_CACHE_MAX_BYTES)\n return\n // Drop any stale entry for this key first so the byte accounting stays exact.\n const prev = fetchCache.get(key)\n if (prev) {\n fetchCache.delete(key)\n fetchCacheBytes -= prev.bytes\n }\n fetchCache.set(key, { output, expiresAt: Date.now() + ttlMs, bytes })\n fetchCacheBytes += bytes\n // Evict oldest-first until both caps are satisfied.\n while (fetchCache.size > FETCH_CACHE_MAX_ENTRIES || fetchCacheBytes > FETCH_CACHE_MAX_BYTES) {\n const oldest = fetchCache.keys().next().value\n if (oldest === undefined)\n break\n const victim = fetchCache.get(oldest)!\n fetchCache.delete(oldest)\n fetchCacheBytes -= victim.bytes\n }\n}\n\n/** Test seam — drop all cached entries. */\nexport function clearFetchUrlCache(): void {\n fetchCache.clear()\n fetchCacheBytes = 0\n}\n\n/**\n * Test seam — exercise the cache get/set/eviction in isolation without\n * standing up the full SSRF + HTTP path. Not part of the public tool surface;\n * `execute` uses the module-private functions directly.\n */\nexport const __fetchCacheTestApi = {\n key: cacheKey,\n get: cacheGet,\n set: cacheSet,\n size: () => fetchCache.size,\n bytes: () => fetchCacheBytes,\n}\n\nexport const fetchUrl: ToolDef = {\n isConcurrencySafe: true,\n spec: {\n name: 'fetch_url',\n description: 'Fetch a public URL and return its body as plain text. HTML is reduced to text (scripts, styles, tags stripped; entities decoded). If a `web_search` tool is also available, use it first to find the URL. Only http(s) URLs are accepted; loopback, link-local, private, and cloud-metadata addresses are refused. Output is capped (default 200 KiB).',\n inputSchema: {\n type: 'object',\n properties: {\n url: {\n type: 'string',\n description: 'HTTP or HTTPS URL to fetch.',\n },\n max_bytes: {\n type: 'number',\n description: `Truncate the response body beyond this many bytes. Default: ${DEFAULT_MAX_BYTES}. Hard cap: ${HARD_MAX_BYTES}.`,\n },\n },\n required: ['url'],\n },\n },\n async execute({ url, max_bytes }, ctx: ToolContext) {\n const raw = typeof url === 'string' ? url.trim() : ''\n if (!raw)\n return 'fetch_url error: `url` is required'\n let parsed: URL\n try {\n parsed = new URL(raw)\n }\n catch {\n return `fetch_url error: invalid URL: ${raw}`\n }\n if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:')\n return `fetch_url error: only http(s) URLs are allowed (got ${parsed.protocol})`\n\n const cap = Math.min(\n typeof max_bytes === 'number' && max_bytes > 0 ? Math.floor(max_bytes) : DEFAULT_MAX_BYTES,\n HARD_MAX_BYTES,\n )\n // Buffer up to 2× the cap so HTML overhead doesn't starve the visible\n // text budget — a 200 KiB cap on rendered text can legitimately need to\n // read ~400 KiB of raw HTML before stripping. Hard ceiling is still\n // governed by `HARD_MAX_BYTES * 2` which is comfortably below OOM.\n const bufferCap = Math.min(cap * 2, HARD_MAX_BYTES * 2)\n\n // Opt-in response cache (off unless `behavior.fetchUrlCacheTtlMs > 0`).\n const ttlMs = ctx.behavior?.fetchUrlCacheTtlMs\n const cacheEnabled = typeof ttlMs === 'number' && ttlMs > 0\n const key = cacheKey(parsed.toString(), cap)\n if (cacheEnabled) {\n const hit = cacheGet(key)\n if (hit !== null)\n return hit\n }\n\n try {\n const res = await fetchWithSsrfGuard(\n parsed,\n {\n 'user-agent': 'Mozilla/5.0 (compatible; zidane/1.0)',\n 'accept': 'text/html,text/plain,application/json;q=0.9,*/*;q=0.5',\n 'accept-encoding': 'identity', // no gzip — we don't decompress\n },\n ctx.signal,\n bufferCap,\n {},\n ctx.behavior?.fetchUrlAllowHosts,\n )\n const body = res.body\n const isHtml = res.contentType.includes('html') || /^\\s*<!doctype html|^\\s*<html/i.test(body)\n const text = isHtml ? stripHtmlBlock(body) : body\n const truncated = text.length > cap\n const out = truncated ? `${text.slice(0, cap)}\\n\\n[truncated at ${cap} bytes; full length ${text.length}]` : text\n // `res.url` reflects the FINAL hop after redirects; flag a cross-host\n // landing so the model knows the content came from a different origin\n // than it asked for (open-redirect awareness). Same-host path changes\n // and www add/strip are treated as the same site and not flagged.\n const finalHost = sameSiteHost(res.url)\n const requestedHost = sameSiteHost(parsed.toString())\n const redirectNote = finalHost && requestedHost && finalHost !== requestedHost\n ? `Note: ${parsed.hostname} redirected to a different host (${new URL(res.url).hostname}).\\n`\n : ''\n const header = `URL: ${res.url}\\nStatus: ${res.status}\\nContent-Type: ${res.contentType || '(none)'}\\n${redirectNote}\\n`\n const formatted = header + out\n // Cache only successful text responses — never errors / redirects.\n if (cacheEnabled && res.status >= 200 && res.status < 300)\n cacheSet(key, formatted, ttlMs)\n return formatted\n }\n catch (err) {\n return `fetch_url error: ${errorMessage(err)}`\n }\n },\n}\n"],"mappings":"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"}
|