@armadra/agent 0.9.3 → 0.11.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 (66) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/CHANGELOG.zh-CN.md +22 -0
  3. package/README.md +5 -0
  4. package/README.zh-CN.md +4 -0
  5. package/dist/bundle/ama.cjs +1606 -423
  6. package/dist/cli/compose.d.ts +3 -0
  7. package/dist/cli/compose.js +1 -0
  8. package/dist/drivers/agents.js +1 -1
  9. package/dist/drivers/host-runners.d.ts +26 -1
  10. package/dist/drivers/host-runners.js +147 -0
  11. package/dist/hooks/matcher.d.ts +2 -2
  12. package/dist/hooks/matcher.js +3 -3
  13. package/dist/host/api-impl.d.ts +3 -0
  14. package/dist/host/api-impl.js +7 -2
  15. package/dist/host/types.d.ts +37 -1
  16. package/dist/host/types.js +5 -0
  17. package/dist/host.d.ts +1 -1
  18. package/dist/host.js +1 -1
  19. package/dist/i18n/catalog.d.ts +5 -0
  20. package/dist/i18n/messages/approval.d.ts +5 -0
  21. package/dist/i18n/messages/approval.js +5 -0
  22. package/dist/i18n/messages/drivers.d.ts +2 -0
  23. package/dist/i18n/messages/drivers.js +2 -0
  24. package/dist/i18n/messages/interactive-view.d.ts +5 -0
  25. package/dist/i18n/messages/interactive-view.js +5 -0
  26. package/dist/i18n/messages/interactive.d.ts +4 -0
  27. package/dist/index.d.ts +1 -1
  28. package/dist/index.js +1 -1
  29. package/dist/modes/commands-core.d.ts +7 -0
  30. package/dist/modes/commands-core.js +23 -5
  31. package/dist/modes/interactive/approval-dialog.js +17 -4
  32. package/dist/modes/interactive/commands.d.ts +2 -0
  33. package/dist/modes/interactive/commands.js +5 -1
  34. package/dist/modes/interactive/interactive-mode.js +2 -2
  35. package/dist/modes/interactive/tool-summary.d.ts +2 -0
  36. package/dist/modes/interactive/tool-summary.js +27 -0
  37. package/dist/modes/rpc/commands.js +4 -4
  38. package/dist/permissions/classifier.js +1 -1
  39. package/dist/permissions/network-class.d.ts +28 -0
  40. package/dist/permissions/network-class.js +91 -0
  41. package/dist/permissions/pipeline.d.ts +1 -1
  42. package/dist/permissions/pipeline.js +24 -7
  43. package/dist/permissions/preview.js +3 -0
  44. package/dist/permissions/rules.d.ts +1 -0
  45. package/dist/permissions/rules.js +5 -1
  46. package/dist/tools/presets.d.ts +1 -1
  47. package/dist/tools/presets.js +2 -2
  48. package/dist/tools/registry.d.ts +3 -1
  49. package/dist/tools/registry.js +4 -2
  50. package/dist/tools/types.d.ts +4 -1
  51. package/dist/tools/web/fetch.d.ts +48 -0
  52. package/dist/tools/web/fetch.js +307 -0
  53. package/dist/tools/web/html-to-markdown.d.ts +31 -0
  54. package/dist/tools/web/html-to-markdown.js +522 -0
  55. package/dist/tools/web/url-guard.d.ts +59 -0
  56. package/dist/tools/web/url-guard.js +221 -0
  57. package/docs/en/guides/permissions.md +23 -1
  58. package/docs/en/reference/host-api.md +61 -19
  59. package/docs/en/reference/rpc.md +7 -7
  60. package/docs/guides/agents.md +14 -0
  61. package/docs/guides/hooks.md +86 -8
  62. package/docs/guides/permissions.md +23 -1
  63. package/docs/guides/sandbox.md +6 -0
  64. package/docs/reference/host-api.md +61 -19
  65. package/docs/reference/rpc.md +7 -7
  66. package/package.json +1 -1
@@ -0,0 +1,522 @@
1
+ /**
2
+ * HTML → Markdown(`web_fetch` 用,零依赖)。宽松的分词 + 轻量树,不做完整 HTML5 解析:
3
+ *
4
+ * - 丢弃 `script` / `style` / `noscript` / `svg` / `template` / `iframe` / `nav` / `footer` / `aside` /
5
+ * `form` / `button` 及其内容,`<!-- -->` 注释与 `<head>`(标题单独取 `<title>`);
6
+ * - 正文优先 `<main>`,其次 `<article>`、`role="main"`,再次 `<body>`;
7
+ * - 保留标题、段落、列表(嵌套缩进)、链接(`[文字](绝对 URL)`,`javascript:` 只留文字)、行内代码与
8
+ * `<pre>` 代码块(`language-xxx` 类名作语言)、引用、简单表格、`<br>` / `<hr>`;图片只留 alt;
9
+ * - 实体:常用命名实体 + 十进制 / 十六进制数字实体;非 `<pre>` 文本折叠空白。
10
+ *
11
+ * 输出只依赖输入,便于黄金测试。
12
+ */
13
+ const VOID = new Set([
14
+ "area",
15
+ "base",
16
+ "br",
17
+ "col",
18
+ "embed",
19
+ "hr",
20
+ "img",
21
+ "input",
22
+ "link",
23
+ "meta",
24
+ "param",
25
+ "source",
26
+ "track",
27
+ "wbr",
28
+ ]);
29
+ const RAW_TEXT = new Set(["script", "style", "textarea", "title", "xmp", "noscript"]);
30
+ const DROP = new Set([
31
+ "script",
32
+ "style",
33
+ "noscript",
34
+ "svg",
35
+ "math",
36
+ "template",
37
+ "iframe",
38
+ "nav",
39
+ "footer",
40
+ "aside",
41
+ "form",
42
+ "button",
43
+ "select",
44
+ "textarea",
45
+ "head",
46
+ "title",
47
+ "canvas",
48
+ "object",
49
+ "dialog",
50
+ ]);
51
+ const BLOCK = new Set([
52
+ "address",
53
+ "article",
54
+ "blockquote",
55
+ "body",
56
+ "center",
57
+ "dd",
58
+ "details",
59
+ "div",
60
+ "dl",
61
+ "dt",
62
+ "figcaption",
63
+ "figure",
64
+ "h1",
65
+ "h2",
66
+ "h3",
67
+ "h4",
68
+ "h5",
69
+ "h6",
70
+ "header",
71
+ "hr",
72
+ "html",
73
+ "li",
74
+ "main",
75
+ "ol",
76
+ "p",
77
+ "pre",
78
+ "section",
79
+ "summary",
80
+ "table",
81
+ "tbody",
82
+ "td",
83
+ "tfoot",
84
+ "th",
85
+ "thead",
86
+ "tr",
87
+ "ul",
88
+ ]);
89
+ /** 打开这些标签时,先关掉还开着的同类(`<li>`、`<p>` 等常省略结束标签)。 */
90
+ const AUTO_CLOSE = {
91
+ li: ["li"],
92
+ dt: ["dt", "dd"],
93
+ dd: ["dt", "dd"],
94
+ tr: ["tr"],
95
+ td: ["td", "th"],
96
+ th: ["td", "th"],
97
+ option: ["option"],
98
+ };
99
+ const CLOSES_P = new Set([
100
+ "p",
101
+ ...[...BLOCK].filter((t) => !["li", "td", "th", "dd", "dt"].includes(t)),
102
+ ]);
103
+ /** 自动关闭不越过这些容器(嵌套列表、表格)。 */
104
+ const SCOPE = new Set(["ul", "ol", "dl", "table", "html", "body"]);
105
+ const ENTITIES = {
106
+ amp: "&",
107
+ lt: "<",
108
+ gt: ">",
109
+ quot: '"',
110
+ apos: "'",
111
+ nbsp: " ",
112
+ copy: "©",
113
+ reg: "®",
114
+ trade: "™",
115
+ hellip: "…",
116
+ mdash: "—",
117
+ ndash: "–",
118
+ lsquo: "‘",
119
+ rsquo: "’",
120
+ ldquo: "“",
121
+ rdquo: "”",
122
+ laquo: "«",
123
+ raquo: "»",
124
+ middot: "·",
125
+ bull: "•",
126
+ times: "×",
127
+ divide: "÷",
128
+ deg: "°",
129
+ euro: "€",
130
+ pound: "£",
131
+ yen: "¥",
132
+ cent: "¢",
133
+ sect: "§",
134
+ para: "¶",
135
+ larr: "←",
136
+ rarr: "→",
137
+ uarr: "↑",
138
+ darr: "↓",
139
+ harr: "↔",
140
+ rArr: "⇒",
141
+ lArr: "⇐",
142
+ le: "≤",
143
+ ge: "≥",
144
+ ne: "≠",
145
+ plusmn: "±",
146
+ minus: "−",
147
+ shy: "",
148
+ zwj: "",
149
+ zwnj: "",
150
+ thinsp: " ",
151
+ ensp: " ",
152
+ emsp: " ",
153
+ check: "✓",
154
+ };
155
+ /** 解码字符实体。 */
156
+ export function decodeEntities(text) {
157
+ return text.replace(/&(#x[0-9a-f]+|#\d+|[a-z][a-z0-9]*);?/gi, (whole, body) => {
158
+ if (body[0] === "#") {
159
+ const code = body[1] === "x" || body[1] === "X" ? parseInt(body.slice(2), 16) : Number(body.slice(1));
160
+ if (!Number.isFinite(code) ||
161
+ code <= 0 ||
162
+ code > 0x10ffff ||
163
+ (code >= 0xd800 && code <= 0xdfff))
164
+ return "\uFFFD";
165
+ return String.fromCodePoint(code);
166
+ }
167
+ const hit = ENTITIES[body] ?? ENTITIES[body.toLowerCase()];
168
+ return hit ?? whole;
169
+ });
170
+ }
171
+ const ATTR_RE = /([^\s"'<>/=]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/g;
172
+ function parseAttrs(text) {
173
+ const attrs = {};
174
+ for (const m of text.matchAll(ATTR_RE)) {
175
+ const name = m[1].toLowerCase();
176
+ if (!(name in attrs))
177
+ attrs[name] = decodeEntities(m[2] ?? m[3] ?? m[4] ?? "");
178
+ }
179
+ return attrs;
180
+ }
181
+ /** 宽松解析成树。 */
182
+ export function parseHtml(html) {
183
+ const root = { tag: "#root", attrs: {}, children: [] };
184
+ let cur = root;
185
+ const open = (tag) => {
186
+ for (let n = cur; n !== undefined; n = n.parent)
187
+ if (n.tag === tag)
188
+ return n;
189
+ return undefined;
190
+ };
191
+ const closeTo = (node) => {
192
+ cur = node.parent ?? root;
193
+ };
194
+ /** 从当前节点往上找还开着的 `tags` 之一并关掉;先碰到 `stop` 里的标签就作罢。 */
195
+ const closeOpen = (tags, stop) => {
196
+ for (let n = cur; n !== undefined && n !== root; n = n.parent) {
197
+ if (tags.includes(n.tag))
198
+ return closeTo(n);
199
+ if (stop.has(n.tag))
200
+ return;
201
+ }
202
+ };
203
+ let i = 0;
204
+ while (i < html.length) {
205
+ const lt = html.indexOf("<", i);
206
+ if (lt === -1) {
207
+ cur.children.push(html.slice(i));
208
+ break;
209
+ }
210
+ if (lt > i)
211
+ cur.children.push(html.slice(i, lt));
212
+ if (html.startsWith("<!--", lt)) {
213
+ const end = html.indexOf("-->", lt + 4);
214
+ i = end === -1 ? html.length : end + 3;
215
+ continue;
216
+ }
217
+ if (html[lt + 1] === "!" || html[lt + 1] === "?") {
218
+ const end = html.indexOf(">", lt);
219
+ i = end === -1 ? html.length : end + 1;
220
+ continue;
221
+ }
222
+ const m = /^<(\/?)([a-zA-Z][a-zA-Z0-9:-]*)((?:[^>"']|"[^"]*"|'[^']*')*)>/.exec(html.slice(lt, lt + 8192));
223
+ if (m === null) {
224
+ cur.children.push("<");
225
+ i = lt + 1;
226
+ continue;
227
+ }
228
+ i = lt + m[0].length;
229
+ const tag = m[2].toLowerCase();
230
+ if (m[1] === "/") {
231
+ const node = open(tag);
232
+ if (node !== undefined)
233
+ closeTo(node);
234
+ continue;
235
+ }
236
+ if (CLOSES_P.has(tag))
237
+ closeOpen(["p"], BLOCK);
238
+ const siblings = AUTO_CLOSE[tag];
239
+ if (siblings !== undefined)
240
+ closeOpen(siblings, SCOPE);
241
+ const node = { tag, attrs: parseAttrs(m[3]), children: [], parent: cur };
242
+ cur.children.push(node);
243
+ if (RAW_TEXT.has(tag)) {
244
+ const end = html.toLowerCase().indexOf(`</${tag}`, i);
245
+ const body = html.slice(i, end === -1 ? html.length : end);
246
+ node.children.push(body);
247
+ const close = end === -1 ? -1 : html.indexOf(">", end);
248
+ i = close === -1 ? html.length : close + 1;
249
+ continue;
250
+ }
251
+ if (!VOID.has(tag) && !/\/\s*$/.test(m[3]))
252
+ cur = node;
253
+ }
254
+ return root;
255
+ }
256
+ function find(node, test) {
257
+ for (const child of node.children) {
258
+ if (typeof child === "string")
259
+ continue;
260
+ if (test(child))
261
+ return child;
262
+ if (DROP.has(child.tag))
263
+ continue;
264
+ const hit = find(child, test);
265
+ if (hit !== undefined)
266
+ return hit;
267
+ }
268
+ return undefined;
269
+ }
270
+ function rawText(node) {
271
+ return node.children.map((c) => (typeof c === "string" ? c : rawText(c))).join("");
272
+ }
273
+ function resolveHref(href, base) {
274
+ const value = href.trim();
275
+ if (value === "" || /^(javascript|data|vbscript):/i.test(value))
276
+ return undefined;
277
+ if (value.startsWith("#"))
278
+ return undefined;
279
+ try {
280
+ return base === undefined ? new URL(value).href : new URL(value, base).href;
281
+ }
282
+ catch {
283
+ return undefined;
284
+ }
285
+ }
286
+ function collapse(text) {
287
+ return decodeEntities(text).replace(/[\s\u00a0]+/g, " ");
288
+ }
289
+ function inline(node, ctx) {
290
+ let out = "";
291
+ for (const child of node.children) {
292
+ if (typeof child === "string")
293
+ out += collapse(child);
294
+ else
295
+ out += renderInline(child, ctx);
296
+ }
297
+ return out;
298
+ }
299
+ function wrap(mark, text) {
300
+ const trimmed = text.trim();
301
+ if (trimmed === "")
302
+ return text;
303
+ const lead = text.startsWith(" ") ? " " : "";
304
+ const trail = text.endsWith(" ") ? " " : "";
305
+ return `${lead}${mark}${trimmed}${mark}${trail}`;
306
+ }
307
+ function renderInline(node, ctx) {
308
+ if (DROP.has(node.tag))
309
+ return "";
310
+ switch (node.tag) {
311
+ case "br":
312
+ return "\n";
313
+ case "img": {
314
+ const alt = (node.attrs["alt"] ?? "").trim();
315
+ return alt === "" ? "" : collapse(alt);
316
+ }
317
+ case "a": {
318
+ const text = inline(node, ctx);
319
+ const href = resolveHref(node.attrs["href"] ?? "", ctx.base);
320
+ if (href === undefined || text.trim() === "")
321
+ return text;
322
+ return `[${text.trim().replace(/[[\]]/g, "\\$&")}](${href.replace(/[()\s]/g, encodeURIComponent)})`;
323
+ }
324
+ case "code":
325
+ case "kbd":
326
+ case "samp":
327
+ case "tt": {
328
+ const text = decodeEntities(rawText(node)).replace(/\s+/g, " ");
329
+ if (text.trim() === "")
330
+ return text;
331
+ const fence = text.includes("`") ? "``" : "`";
332
+ return `${fence}${text}${fence}`;
333
+ }
334
+ case "strong":
335
+ case "b":
336
+ return wrap("**", inline(node, ctx));
337
+ case "em":
338
+ case "i":
339
+ return wrap("*", inline(node, ctx));
340
+ case "del":
341
+ case "s":
342
+ return wrap("~~", inline(node, ctx));
343
+ default:
344
+ return BLOCK.has(node.tag) ? `\n\n${blocks(node, ctx)}\n\n` : inline(node, ctx);
345
+ }
346
+ }
347
+ /** 子节点渲染成块序列(块之间空一行)。 */
348
+ function blocks(node, ctx) {
349
+ const parts = [];
350
+ let run = "";
351
+ const flush = () => {
352
+ const text = run
353
+ .split("\n")
354
+ .map((l) => l.replace(/ {2,}/g, " ").trim())
355
+ .join("\n")
356
+ .trim();
357
+ if (text !== "")
358
+ parts.push(text);
359
+ run = "";
360
+ };
361
+ for (const child of node.children) {
362
+ if (typeof child === "string") {
363
+ run += collapse(child);
364
+ continue;
365
+ }
366
+ if (DROP.has(child.tag))
367
+ continue;
368
+ if (BLOCK.has(child.tag)) {
369
+ flush();
370
+ const text = renderBlock(child, ctx);
371
+ if (text.trim() !== "")
372
+ parts.push(text);
373
+ }
374
+ else
375
+ run += renderInline(child, ctx);
376
+ }
377
+ flush();
378
+ return parts.join("\n\n");
379
+ }
380
+ function prefixLines(text, first, rest) {
381
+ return text
382
+ .split("\n")
383
+ .map((line, i) => line === "" ? (i === 0 ? first.trimEnd() : "") : `${i === 0 ? first : rest}${line}`)
384
+ .join("\n");
385
+ }
386
+ function renderList(node, ctx) {
387
+ const ordered = node.tag === "ol";
388
+ let n = Number(node.attrs["start"] ?? "1");
389
+ if (!Number.isFinite(n))
390
+ n = 1;
391
+ const items = [];
392
+ const inner = { ...ctx, listDepth: ctx.listDepth + 1 };
393
+ for (const child of node.children) {
394
+ if (typeof child === "string" || DROP.has(child.tag))
395
+ continue;
396
+ const body = child.tag === "li" ? blocks(child, inner) : renderBlock(child, inner);
397
+ if (body.trim() === "")
398
+ continue;
399
+ const marker = ordered ? `${n++}. ` : "- ";
400
+ items.push(prefixLines(body.replace(/\n{2,}/g, "\n"), marker, " ".repeat(marker.length)));
401
+ }
402
+ return items.join("\n");
403
+ }
404
+ function cellText(node, ctx) {
405
+ return blocks(node, ctx)
406
+ .replace(/\s*\n+\s*/g, " ")
407
+ .replace(/\|/g, "\\|")
408
+ .trim();
409
+ }
410
+ function renderTable(node, ctx) {
411
+ const rows = [];
412
+ const walk = (n) => {
413
+ for (const child of n.children) {
414
+ if (typeof child === "string" || DROP.has(child.tag))
415
+ continue;
416
+ if (child.tag === "tr") {
417
+ const cells = child.children.filter((c) => typeof c !== "string" && (c.tag === "td" || c.tag === "th"));
418
+ if (cells.length > 0)
419
+ rows.push({
420
+ cells: cells.map((c) => cellText(c, ctx)),
421
+ header: n.tag === "thead" || cells.every((c) => c.tag === "th"),
422
+ });
423
+ }
424
+ else if (child.tag !== "table")
425
+ walk(child);
426
+ }
427
+ };
428
+ walk(node);
429
+ if (rows.length === 0)
430
+ return "";
431
+ const width = Math.max(...rows.map((r) => r.cells.length));
432
+ const line = (cells) => `| ${[...cells, ...new Array(width - cells.length).fill("")].join(" | ")} |`;
433
+ const headerIndex = rows[0]?.header === true ? 0 : -1;
434
+ const head = headerIndex === 0
435
+ ? rows.shift().cells
436
+ : new Array(width).fill("");
437
+ return [line(head), `|${" --- |".repeat(width)}`, ...rows.map((r) => line(r.cells))].join("\n");
438
+ }
439
+ function renderBlock(node, ctx) {
440
+ switch (node.tag) {
441
+ case "h1":
442
+ case "h2":
443
+ case "h3":
444
+ case "h4":
445
+ case "h5":
446
+ case "h6": {
447
+ const text = inline(node, ctx).replace(/\s+/g, " ").trim();
448
+ return text === "" ? "" : `${"#".repeat(Number(node.tag[1]))} ${text}`;
449
+ }
450
+ case "hr":
451
+ return "---";
452
+ case "pre": {
453
+ const code = find(node, (n) => n.tag === "code");
454
+ const cls = `${code?.attrs["class"] ?? ""} ${node.attrs["class"] ?? ""}`;
455
+ const lang = /(?:^|\s)(?:language|lang)-([\w+#-]+)/.exec(cls)?.[1] ?? "";
456
+ const text = decodeEntities(rawText(node)).replace(/^\n/, "").replace(/\s+$/, "");
457
+ const fence = text.includes("```") ? "~~~~" : "```";
458
+ return `${fence}${lang}\n${text}\n${fence}`;
459
+ }
460
+ case "blockquote":
461
+ return prefixLines(blocks(node, ctx), "> ", "> ");
462
+ case "ul":
463
+ case "ol":
464
+ return renderList(node, ctx);
465
+ case "table":
466
+ return renderTable(node, ctx);
467
+ default:
468
+ return blocks(node, ctx);
469
+ }
470
+ }
471
+ /** 页面标题:`<title>`,没有时取第一个 `<h1>`。 */
472
+ function pageTitle(root) {
473
+ const title = find(root, (n) => n.tag === "title") ?? findIn(root, "title");
474
+ const text = title !== undefined ? collapse(rawText(title)).trim() : "";
475
+ if (text !== "")
476
+ return text;
477
+ const h1 = find(root, (n) => n.tag === "h1");
478
+ const heading = h1 !== undefined ? collapse(rawText(h1)).trim() : "";
479
+ return heading === "" ? undefined : heading;
480
+ }
481
+ /** 在丢弃的子树(`<head>`)里也找。 */
482
+ function findIn(node, tag) {
483
+ for (const child of node.children) {
484
+ if (typeof child === "string")
485
+ continue;
486
+ if (child.tag === tag)
487
+ return child;
488
+ const hit = findIn(child, tag);
489
+ if (hit !== undefined)
490
+ return hit;
491
+ }
492
+ return undefined;
493
+ }
494
+ /** HTML 全文 → `{ title, markdown }`。`baseUrl` 用来把相对链接变成绝对 URL。 */
495
+ export function htmlToMarkdown(html, baseUrl) {
496
+ const root = parseHtml(html);
497
+ let base;
498
+ try {
499
+ const declared = findIn(root, "base")?.attrs["href"];
500
+ base = baseUrl === undefined ? undefined : new URL(declared ?? "", baseUrl);
501
+ }
502
+ catch {
503
+ base = baseUrl === undefined ? undefined : new URL(baseUrl);
504
+ }
505
+ const main = find(root, (n) => n.tag === "main") ??
506
+ find(root, (n) => n.tag === "article") ??
507
+ find(root, (n) => n.attrs["role"] === "main") ??
508
+ find(root, (n) => n.tag === "body") ??
509
+ root;
510
+ const markdown = blocks(main, { base, listDepth: 0 })
511
+ .replace(/[ \t]+\n/g, "\n")
512
+ .replace(/\n{3,}/g, "\n\n")
513
+ .trim();
514
+ const title = pageTitle(root);
515
+ return title === undefined ? { markdown } : { title, markdown };
516
+ }
517
+ /** `<meta charset>` / `http-equiv` 里声明的编码(只看前 2 KB)。 */
518
+ export function sniffMetaCharset(head) {
519
+ const text = head.slice(0, 2048);
520
+ const direct = /<meta[^>]+charset\s*=\s*["']?\s*([\w.:-]+)/i.exec(text);
521
+ return direct?.[1]?.toLowerCase();
522
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * `web_fetch` 的地址守卫(docs/guides/permissions.md「网络类」):URL 校验、私网 / 元数据地址判定、
3
+ * DNS 钉住与重定向策略。纯 `node:*`,模型侧固定英文。
4
+ *
5
+ * - URL:只收 `http:` / `https:`;拒 userinfo;全文 ≤ 2 048 字符、查询串 ≤ 1 024 字符;主机名必须含 `.`
6
+ * (`localhost`、`*.localhost` 与无点主机一律拒——本机服务请走 bash 审批);IP 字面量当场判定。
7
+ * - 地址:解析出的**全部**地址都不得落在环回、未指定、链路本地(含云元数据 `169.254.169.254`)、
8
+ * RFC 1918、运营商 NAT `100.64/10`、基准测试 `198.18/15`、保留与广播、组播、IPv6 ULA `fc00::/7`、
9
+ * 站点本地 `fec0::/10`、链路本地 `fe80::/10`、组播 `ff00::/8`、未指定与已弃用的兼容段 `::/96`;IPv4 映射 / 兼容、NAT64 `64:ff9b::/96`、
10
+ * 6to4 `2002::/16` 里嵌的 IPv4 按 IPv4 再判一次。有一个落在里面就拒(防「多地址里夹一个内网」)。
11
+ * - DNS 钉住:连接用 `pinnedLookup()` 只返回已检查过的地址,防检查与连接之间被重新解析(DNS 重绑定)。
12
+ * - 重定向:同主机(主机名与端口相同、不从 https 降到 http)由调用方跟随并逐跳重检;跨主机不跟。
13
+ */
14
+ import { type LookupAddress } from "node:dns";
15
+ export declare const MAX_URL_LENGTH = 2048;
16
+ export declare const MAX_QUERY_LENGTH = 1024;
17
+ export declare const MAX_SAME_HOST_REDIRECTS = 5;
18
+ /** IPv6 文本 → 8 个 16 位组;不合法返回 undefined。支持 `::` 与末尾点分 IPv4。 */
19
+ export declare function ipv6Groups(address: string): number[] | undefined;
20
+ /** IPv6 里嵌的 IPv4(映射 / 兼容 / NAT64 / 6to4);没有返回 undefined。 */
21
+ export declare function embeddedIPv4(address: string): string | undefined;
22
+ /**
23
+ * 地址是否落在禁止段;落在里面返回英文原因(进工具结果),否则 undefined。
24
+ * 不是 IP 文本的一律按禁止(调用方只该传解析结果)。
25
+ */
26
+ export declare function blockedAddressReason(address: string): string | undefined;
27
+ export type UrlCheck = {
28
+ ok: true;
29
+ url: URL;
30
+ hostname: string;
31
+ } | {
32
+ ok: false;
33
+ reason: string;
34
+ };
35
+ /** 主机名:去方括号与末尾点,小写。 */
36
+ export declare function bareHostname(url: URL): string;
37
+ /** 静态校验(不查 DNS):协议、userinfo、长度、主机名形状、IP 字面量。 */
38
+ export declare function checkFetchUrl(raw: string): UrlCheck;
39
+ export type LookupAll = (hostname: string) => Promise<LookupAddress[]>;
40
+ /** 缺省解析:系统解析器,返回全部地址。 */
41
+ export declare const systemLookup: LookupAll;
42
+ export type ResolveCheck = {
43
+ ok: true;
44
+ addresses: LookupAddress[];
45
+ } | {
46
+ ok: false;
47
+ reason: string;
48
+ };
49
+ /** 解析主机名并检查全部地址;IP 字面量直接返回。 */
50
+ export declare function resolvePublicAddresses(hostname: string, lookup?: LookupAll): Promise<ResolveCheck>;
51
+ type LookupCallback = (err: NodeJS.ErrnoException | null, address: string | LookupAddress[], family?: number) => void;
52
+ /** `http.request({ lookup })` 用:只返回已检查过的地址(兼容 `all: true` 与单地址两种调用)。 */
53
+ export declare function pinnedLookup(addresses: readonly LookupAddress[]): (_hostname: string, options: unknown, callback?: LookupCallback) => void;
54
+ /**
55
+ * 重定向能否自动跟:同主机名,且同协议同端口或 http → https 升级。其余(跨主机、换端口、
56
+ * https 降到 http)不跟:交给模型再调一次 web_fetch,下一次调用重新过权限。
57
+ */
58
+ export declare function redirectPolicy(from: URL, to: URL): "follow" | "cross-host";
59
+ export {};