@pugi/cli 0.1.0-beta.5 → 0.1.0-beta.50
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/THIRD_PARTY_NOTICES.md +40 -0
- package/assets/pugi-mascot.ansi +15 -25
- package/assets/pugi-prozr2-mascot.ansi +9 -0
- package/bin/run.js +33 -1
- package/dist/commands/jobs-watch.js +201 -0
- package/dist/commands/jobs.js +15 -0
- package/dist/commands/smoke.js +133 -0
- package/dist/core/agent-progress/cleanup.js +134 -0
- package/dist/core/agent-progress/schema.js +144 -0
- package/dist/core/agent-progress/writer.js +101 -0
- package/dist/core/artifact-chain/dispatcher.js +148 -0
- package/dist/core/artifact-chain/exporter.js +164 -0
- package/dist/core/artifact-chain/state.js +243 -0
- package/dist/core/artifact-chain/steps.js +169 -0
- package/dist/core/auth/ensure-authenticated.js +129 -0
- package/dist/core/auth/env-provider.js +238 -0
- package/dist/core/auto-update/channels.js +122 -0
- package/dist/core/auto-update/checker.js +241 -0
- package/dist/core/auto-update/state.js +235 -0
- package/dist/core/bare-mode/index.js +107 -0
- package/dist/core/bash-classifier.js +400 -4
- package/dist/core/checkpoint/resumer.js +149 -0
- package/dist/core/checkpoint/rewinder.js +291 -0
- package/dist/core/codegraph/decision-store.js +248 -0
- package/dist/core/codegraph/detect-repo.js +459 -0
- package/dist/core/codegraph/install.js +134 -0
- package/dist/core/codegraph/offer-hook.js +220 -0
- package/dist/core/compact/auto-trigger.js +96 -0
- package/dist/core/compact/buffer-rewriter.js +115 -0
- package/dist/core/compact/summarizer.js +208 -0
- package/dist/core/compact/token-counter.js +108 -0
- package/dist/core/consensus/diff-capture.js +112 -3
- package/dist/core/context/index.js +7 -0
- package/dist/core/context/markdown-traverse.js +255 -0
- package/dist/core/cost/rate-card.js +129 -0
- package/dist/core/cost/tracker.js +221 -0
- package/dist/core/denial-tracking/index.js +8 -0
- package/dist/core/denial-tracking/state.js +264 -0
- package/dist/core/diagnostics/probe-runner.js +93 -0
- package/dist/core/diagnostics/probes/api.js +46 -0
- package/dist/core/diagnostics/probes/auth.js +86 -0
- package/dist/core/diagnostics/probes/bare-mode.js +42 -0
- package/dist/core/diagnostics/probes/cli-version.js +127 -0
- package/dist/core/diagnostics/probes/config.js +72 -0
- package/dist/core/diagnostics/probes/denial-tracking.js +57 -0
- package/dist/core/diagnostics/probes/disk.js +81 -0
- package/dist/core/diagnostics/probes/git.js +65 -0
- package/dist/core/diagnostics/probes/hooks.js +118 -0
- package/dist/core/diagnostics/probes/mcp.js +75 -0
- package/dist/core/diagnostics/probes/node.js +59 -0
- package/dist/core/diagnostics/probes/pnpm.js +36 -0
- package/dist/core/diagnostics/probes/pugi-md.js +89 -0
- package/dist/core/diagnostics/probes/sandbox.js +40 -0
- package/dist/core/diagnostics/probes/session.js +74 -0
- package/dist/core/diagnostics/probes/status-snapshot.js +488 -0
- package/dist/core/diagnostics/probes/workspace.js +63 -0
- package/dist/core/diagnostics/types.js +70 -0
- package/dist/core/dispatch/cache-cleanup.js +197 -0
- package/dist/core/dispatch/cache-handoff.js +295 -0
- package/dist/core/edits/dispatch.js +218 -2
- package/dist/core/edits/journal.js +199 -0
- package/dist/core/edits/layer-d-ast.js +557 -14
- package/dist/core/edits/verify-hook.js +273 -0
- package/dist/core/edits/worktree.js +322 -0
- package/dist/core/engine/anvil-client.js +115 -5
- package/dist/core/engine/budgets.js +98 -0
- package/dist/core/engine/context-prefix.js +155 -0
- package/dist/core/engine/intent.js +260 -0
- package/dist/core/engine/native-pugi.js +860 -211
- package/dist/core/engine/prompts.js +88 -2
- package/dist/core/engine/strip-internal-fields.js +124 -0
- package/dist/core/engine/tool-bridge.js +1045 -36
- package/dist/core/feedback/queue.js +177 -0
- package/dist/core/feedback/submitter.js +145 -0
- package/dist/core/file-cache.js +113 -1
- package/dist/core/hooks/events.js +44 -0
- package/dist/core/hooks/index.js +15 -0
- package/dist/core/hooks/registry.js +213 -0
- package/dist/core/hooks/runner.js +236 -0
- package/dist/core/hooks/v2/event-emitter.js +115 -0
- package/dist/core/hooks/v2/executor.js +282 -0
- package/dist/core/hooks/v2/index.js +25 -0
- package/dist/core/hooks/v2/lifecycle.js +104 -0
- package/dist/core/hooks/v2/loader.js +216 -0
- package/dist/core/hooks/v2/matcher.js +125 -0
- package/dist/core/hooks/v2/trust.js +143 -0
- package/dist/core/hooks/v2/types.js +86 -0
- package/dist/core/lsp/cache.js +105 -0
- package/dist/core/lsp/client.js +776 -0
- package/dist/core/lsp/language-detect.js +66 -0
- package/dist/core/lsp/post-edit-diagnostics.js +171 -0
- package/dist/core/mcp/client.js +75 -6
- package/dist/core/mcp/http-server.js +553 -0
- package/dist/core/mcp/orchestrator-tools.js +662 -0
- package/dist/core/mcp/permission.js +190 -0
- package/dist/core/mcp/registry.js +24 -2
- package/dist/core/mcp/server-tools.js +219 -0
- package/dist/core/mcp/server.js +397 -0
- package/dist/core/memory/dual-write.js +416 -0
- package/dist/core/memory/phase1-kinds.js +20 -0
- package/dist/core/memory-sync/queue.js +158 -0
- package/dist/core/onboarding/ensure-initialized.js +133 -0
- package/dist/core/onboarding/marker.js +111 -0
- package/dist/core/onboarding/telemetry-state.js +108 -0
- package/dist/core/output-style/presets.js +176 -0
- package/dist/core/output-style/state.js +185 -0
- package/dist/core/path-security.js +284 -2
- package/dist/core/permissions/auto-classifier.js +124 -0
- package/dist/core/permissions/circuit-breaker.js +83 -0
- package/dist/core/permissions/gate.js +278 -0
- package/dist/core/permissions/index.js +20 -0
- package/dist/core/permissions/mode.js +174 -0
- package/dist/core/permissions/state.js +241 -0
- package/dist/core/permissions/tool-class.js +93 -0
- package/dist/core/prd-check/parser.js +215 -0
- package/dist/core/prd-check/reporter.js +127 -0
- package/dist/core/prd-check/session-review.js +557 -0
- package/dist/core/prd-check/verifiers.js +223 -0
- package/dist/core/pugi-md/context-injector.js +76 -0
- package/dist/core/pugi-md/walk-up.js +207 -0
- package/dist/core/release-notes/parser.js +241 -0
- package/dist/core/release-notes/state.js +116 -0
- package/dist/core/repl/history.js +11 -1
- package/dist/core/repl/model-pricing.js +135 -0
- package/dist/core/repl/session.js +1897 -37
- package/dist/core/repl/slash-commands.js +430 -15
- package/dist/core/repl/store/session-store.js +31 -2
- package/dist/core/repl/workspace-context.js +22 -0
- package/dist/core/repo-map/build.js +125 -0
- package/dist/core/repo-map/cache.js +185 -0
- package/dist/core/repo-map/extractor.js +254 -0
- package/dist/core/repo-map/formatter.js +145 -0
- package/dist/core/repo-map/scanner.js +211 -0
- package/dist/core/retry-budget/budget.js +284 -0
- package/dist/core/retry-budget/index.js +5 -0
- package/dist/core/session.js +92 -0
- package/dist/core/settings.js +80 -0
- package/dist/core/share/formatter.js +271 -0
- package/dist/core/share/redactor.js +221 -0
- package/dist/core/share/uploader.js +267 -0
- package/dist/core/skills/defaults.js +457 -0
- package/dist/core/smoke/headless-driver.js +174 -0
- package/dist/core/smoke/orchestrator.js +194 -0
- package/dist/core/smoke/runner.js +238 -0
- package/dist/core/smoke/scenario-parser.js +316 -0
- package/dist/core/subagents/dispatcher-real.js +600 -0
- package/dist/core/subagents/dispatcher.js +113 -24
- package/dist/core/subagents/index.js +18 -5
- package/dist/core/subagents/isolation-matrix.js +213 -0
- package/dist/core/subagents/spawn.js +19 -4
- package/dist/core/telemetry/emitter.js +229 -0
- package/dist/core/telemetry/queue.js +251 -0
- package/dist/core/theme/context.js +91 -0
- package/dist/core/theme/presets.js +228 -0
- package/dist/core/theme/state.js +181 -0
- package/dist/core/todos/invariant.js +10 -0
- package/dist/core/todos/state.js +177 -0
- package/dist/core/transport/version-interceptor.js +166 -0
- package/dist/core/vim/keymap.js +288 -0
- package/dist/core/vim/state.js +92 -0
- package/dist/core/worktree-manager/cleanup.js +123 -0
- package/dist/core/worktree-manager/manager.js +303 -0
- package/dist/index.js +28 -0
- package/dist/runtime/bootstrap.js +190 -0
- package/dist/runtime/cli.js +3241 -343
- package/dist/runtime/commands/cancel.js +231 -0
- package/dist/runtime/commands/chain.js +489 -0
- package/dist/runtime/commands/codegraph-status.js +227 -0
- package/dist/runtime/commands/compact.js +297 -0
- package/dist/runtime/commands/cost.js +199 -0
- package/dist/runtime/commands/delegate.js +242 -11
- package/dist/runtime/commands/dispatch.js +126 -0
- package/dist/runtime/commands/doctor.js +412 -0
- package/dist/runtime/commands/feedback.js +184 -0
- package/dist/runtime/commands/hooks.js +184 -0
- package/dist/runtime/commands/lsp.js +368 -0
- package/dist/runtime/commands/mcp.js +879 -0
- package/dist/runtime/commands/memory.js +508 -0
- package/dist/runtime/commands/model.js +237 -0
- package/dist/runtime/commands/onboarding.js +275 -0
- package/dist/runtime/commands/patch.js +128 -0
- package/dist/runtime/commands/permissions.js +112 -0
- package/dist/runtime/commands/plan.js +143 -0
- package/dist/runtime/commands/prd-check.js +285 -0
- package/dist/runtime/commands/redo-blob-store.js +92 -0
- package/dist/runtime/commands/redo.js +361 -0
- package/dist/runtime/commands/release-notes.js +229 -0
- package/dist/runtime/commands/repo-map.js +95 -0
- package/dist/runtime/commands/report.js +299 -0
- package/dist/runtime/commands/resume.js +118 -0
- package/dist/runtime/commands/review-consensus.js +17 -2
- package/dist/runtime/commands/rewind.js +333 -0
- package/dist/runtime/commands/sessions.js +163 -0
- package/dist/runtime/commands/share.js +316 -0
- package/dist/runtime/commands/status.js +186 -0
- package/dist/runtime/commands/stickers.js +82 -0
- package/dist/runtime/commands/style.js +194 -0
- package/dist/runtime/commands/theme.js +196 -0
- package/dist/runtime/commands/undo.js +32 -0
- package/dist/runtime/commands/update.js +289 -0
- package/dist/runtime/commands/vim.js +140 -0
- package/dist/runtime/commands/worktree.js +177 -0
- package/dist/runtime/commands/worktrees.js +155 -0
- package/dist/runtime/headless-repl.js +195 -0
- package/dist/runtime/headless.js +543 -0
- package/dist/runtime/load-hooks-or-exit.js +71 -0
- package/dist/runtime/plan-decompose.js +531 -0
- package/dist/runtime/version.js +65 -0
- package/dist/tools/agent-tool.js +229 -0
- package/dist/tools/apply-patch.js +556 -0
- package/dist/tools/ask-user-question.js +213 -0
- package/dist/tools/ask-user.js +115 -0
- package/dist/tools/bash.js +203 -4
- package/dist/tools/file-tools.js +85 -14
- package/dist/tools/lsp-tools.js +189 -0
- package/dist/tools/mcp-tool.js +260 -0
- package/dist/tools/multi-edit.js +361 -0
- package/dist/tools/powershell.js +268 -0
- package/dist/tools/registry.js +51 -0
- package/dist/tools/skill-tool.js +96 -0
- package/dist/tools/tasks.js +208 -0
- package/dist/tools/todo-write.js +184 -0
- package/dist/tools/web-fetch.js +147 -2
- package/dist/tools/web-search.js +458 -0
- package/dist/tui/agent-progress-card.js +111 -0
- package/dist/tui/agent-tree.js +10 -0
- package/dist/tui/ask-modal.js +2 -2
- package/dist/tui/ask-user-question-prompt.js +192 -0
- package/dist/tui/compact-banner.js +81 -0
- package/dist/tui/conversation-pane.js +82 -8
- package/dist/tui/cost-table.js +111 -0
- package/dist/tui/doctor-table.js +46 -0
- package/dist/tui/feedback-prompt.js +156 -0
- package/dist/tui/input-box.js +218 -3
- package/dist/tui/markdown-render.js +4 -4
- package/dist/tui/onboarding-wizard.js +240 -0
- package/dist/tui/permissions-picker.js +86 -0
- package/dist/tui/render.js +35 -0
- package/dist/tui/repl-render.js +313 -35
- package/dist/tui/repl-splash-art.js +1 -1
- package/dist/tui/repl-splash-mascot.js +32 -8
- package/dist/tui/repl-splash.js +2 -2
- package/dist/tui/repl.js +85 -5
- package/dist/tui/splash.js +1 -1
- package/dist/tui/status-bar.js +94 -16
- package/dist/tui/status-table.js +7 -0
- package/dist/tui/stickers-art.js +136 -0
- package/dist/tui/style-table.js +28 -0
- package/dist/tui/theme-table.js +29 -0
- package/dist/tui/thinking-spinner.js +123 -0
- package/dist/tui/tool-stream-pane.js +52 -3
- package/dist/tui/update-banner.js +27 -2
- package/dist/tui/vim-input.js +267 -0
- package/dist/tui/welcome-banner.js +107 -0
- package/dist/tui/welcome-data.js +293 -0
- package/docs/examples/codegraph.mcp.json +10 -0
- package/package.json +12 -6
- package/test/scenarios/codegen-create-file.scenario.txt +13 -0
- package/test/scenarios/compact-force.scenario.txt +11 -0
- package/test/scenarios/identity.scenario.txt +11 -0
- package/test/scenarios/persona-handoff.scenario.txt +11 -0
- package/test/scenarios/walkback.scenario.txt +12 -0
- package/dist/core/engine/compaction-hook.js +0 -154
package/dist/tools/web-fetch.js
CHANGED
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
* Brand voice: brief / dispatch / ship / sentinel only. The
|
|
35
35
|
* brandbook §08 forbidden-word list applies — see CLAUDE.md.
|
|
36
36
|
*/
|
|
37
|
-
import { request } from 'undici';
|
|
37
|
+
import { request, Agent } from 'undici';
|
|
38
38
|
import { Readability } from '@mozilla/readability';
|
|
39
39
|
import { parseHTML } from 'linkedom';
|
|
40
40
|
import TurndownService from 'turndown';
|
|
@@ -45,6 +45,72 @@ let activeLookup = async (hostname) => await dnsLookup(hostname, { all: true, ve
|
|
|
45
45
|
export function _setLookupForTests(fn) {
|
|
46
46
|
activeLookup = fn ?? (async (hostname) => await dnsLookup(hostname, { all: true, verbatim: true }));
|
|
47
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* β1b #62 — DNS rebinding guard via pinned-address Dispatcher.
|
|
50
|
+
*
|
|
51
|
+
* Without this, the SSRF guard's `dns.lookup` and undici's `request()`
|
|
52
|
+
* connect(2) each issue independent DNS queries. A hostile resolver
|
|
53
|
+
* can answer "8.8.8.8" the first time (passes the SSRF guard) and
|
|
54
|
+
* "127.0.0.1" the second time (kernel connects to local metadata).
|
|
55
|
+
*
|
|
56
|
+
* Fix: resolve once, validate, then pin the resolved address into a
|
|
57
|
+
* per-call `Agent` via `connect.lookup`. The connect() path no longer
|
|
58
|
+
* touches DNS — it uses the IP we already approved.
|
|
59
|
+
*
|
|
60
|
+
* Test seam: spec suite uses MockAgent as the global dispatcher; the
|
|
61
|
+
* MockAgent path does not exercise real connect(), so pinning is both
|
|
62
|
+
* pointless and would break the MockAgent stub. Specs flip
|
|
63
|
+
* `_disablePinnedDispatcherForTests(true)` in beforeEach to keep the
|
|
64
|
+
* MockAgent flow intact while production hits the pinned path.
|
|
65
|
+
*/
|
|
66
|
+
let pinnedDispatcherDisabled = false;
|
|
67
|
+
export function _disablePinnedDispatcherForTests(disabled) {
|
|
68
|
+
pinnedDispatcherDisabled = disabled;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Build a per-call undici Agent that always returns the pre-resolved
|
|
72
|
+
* `address` from its connect.lookup hook. Returns `undefined` when the
|
|
73
|
+
* test flag disabled pinning — caller then falls back to the global
|
|
74
|
+
* dispatcher (MockAgent or production default).
|
|
75
|
+
*/
|
|
76
|
+
async function buildPinnedDispatcher(hostname) {
|
|
77
|
+
if (pinnedDispatcherDisabled)
|
|
78
|
+
return undefined;
|
|
79
|
+
// Skip pinning when hostname is already a literal IP — there is no
|
|
80
|
+
// DNS step to race in that case.
|
|
81
|
+
if (isIPv4(hostname) || isIPv6(hostname))
|
|
82
|
+
return undefined;
|
|
83
|
+
let answers;
|
|
84
|
+
try {
|
|
85
|
+
answers = await activeLookup(hostname);
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
// Best-effort — fall through without pinning; the SSRF guard will
|
|
89
|
+
// emit the canonical DNS-lookup-failed error on the caller's path.
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
const pinned = answers[0];
|
|
93
|
+
if (!pinned)
|
|
94
|
+
return undefined;
|
|
95
|
+
// β1b r1: close the DNS rebinding window the original guard could
|
|
96
|
+
// not see. `validateHostnameForFetch` already ran one lookup; the
|
|
97
|
+
// call above is a SECOND lookup whose answer feeds the pin. A
|
|
98
|
+
// hostile resolver can return a public address to the guard and a
|
|
99
|
+
// private address here — re-validate the pinned literal before we
|
|
100
|
+
// hand it to the Agent. Throws so the caller surfaces a security
|
|
101
|
+
// refusal rather than silently dispatching to the wrong host.
|
|
102
|
+
const ipCheck = validateIpLiteralForFetch(pinned.address, pinned.family);
|
|
103
|
+
if (ipCheck !== null) {
|
|
104
|
+
throw new Error(`ssrf_pinned_address_blocked: ${ipCheck}`);
|
|
105
|
+
}
|
|
106
|
+
return new Agent({
|
|
107
|
+
connect: {
|
|
108
|
+
lookup: (_h, _opts, cb) => {
|
|
109
|
+
cb(null, pinned.address, pinned.family);
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
}
|
|
48
114
|
const FETCH_TIMEOUT_MS = 10_000;
|
|
49
115
|
const MAX_RESPONSE_BYTES = 5 * 1024 * 1024; // 5 MiB
|
|
50
116
|
const MAX_REDIRECTS = 5;
|
|
@@ -231,6 +297,42 @@ function ipv4IsBlocked(ip) {
|
|
|
231
297
|
}
|
|
232
298
|
return false;
|
|
233
299
|
}
|
|
300
|
+
/**
|
|
301
|
+
* Validate a single IP literal (v4 or v6) against the SSRF blocklist.
|
|
302
|
+
* Pure synchronous check — no DNS. Returns `null` on success (safe to
|
|
303
|
+
* connect), an error string when the address is blocked or not a
|
|
304
|
+
* recognized IP literal.
|
|
305
|
+
*
|
|
306
|
+
* Used by the pinned-dispatcher path (web-fetch + web-search) to
|
|
307
|
+
* RE-VALIDATE the address actually pinned into `connect.lookup` AFTER
|
|
308
|
+
* the second DNS round-trip. Without this check the original SSRF
|
|
309
|
+
* guard's lookup answers can diverge from the lookup answers that
|
|
310
|
+
* feed the pin (hostile resolver flips public→private between calls);
|
|
311
|
+
* re-checking the pinned literal closes that window.
|
|
312
|
+
*
|
|
313
|
+
* Exported for spec coverage.
|
|
314
|
+
*/
|
|
315
|
+
export function validateIpLiteralForFetch(address, family) {
|
|
316
|
+
if (!address)
|
|
317
|
+
return 'empty address';
|
|
318
|
+
// Trust family hint when present (LookupAddress.family is 4 or 6),
|
|
319
|
+
// otherwise infer from the string shape.
|
|
320
|
+
const isV4 = family === 4 || (family === undefined && isIPv4(address));
|
|
321
|
+
const isV6 = family === 6 || (family === undefined && isIPv6(address));
|
|
322
|
+
if (isV4) {
|
|
323
|
+
if (ipv4IsBlocked(address)) {
|
|
324
|
+
return `IP ${address} is in a blocked range (SSRF guard)`;
|
|
325
|
+
}
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
if (isV6) {
|
|
329
|
+
if (ipv6IsBlocked(address)) {
|
|
330
|
+
return `IPv6 ${address} is in a blocked range (SSRF guard)`;
|
|
331
|
+
}
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
return `address ${address} is not a recognized IPv4/IPv6 literal`;
|
|
335
|
+
}
|
|
234
336
|
/**
|
|
235
337
|
* Resolve `hostname` via dns.lookup and reject if any answer maps to
|
|
236
338
|
* a private/loopback/link-local/CGNAT range. Returns `null` on success
|
|
@@ -395,10 +497,34 @@ export async function webFetchTool(input, ctx) {
|
|
|
395
497
|
let currentUrl = parsedUrl;
|
|
396
498
|
let hops = 0;
|
|
397
499
|
const controller = new AbortController();
|
|
500
|
+
// β1b #62: per-hop pinned Agent so the post-lookup connect(2) cannot
|
|
501
|
+
// be redirected to a private IP by a hostile resolver. Built lazily
|
|
502
|
+
// per hop because each redirect target may resolve to a different
|
|
503
|
+
// host. `undefined` falls back to the global dispatcher (spec
|
|
504
|
+
// MockAgent or production default), preserving the existing test
|
|
505
|
+
// path. The current Agent is closed at end-of-call so we do not leak
|
|
506
|
+
// open connections.
|
|
507
|
+
let activeAgent;
|
|
508
|
+
const closeActiveAgent = async () => {
|
|
509
|
+
if (activeAgent) {
|
|
510
|
+
try {
|
|
511
|
+
await activeAgent.close();
|
|
512
|
+
}
|
|
513
|
+
catch {
|
|
514
|
+
/* ignore — agent already closed */
|
|
515
|
+
}
|
|
516
|
+
activeAgent = undefined;
|
|
517
|
+
}
|
|
518
|
+
};
|
|
398
519
|
try {
|
|
399
520
|
while (true) {
|
|
521
|
+
// β1b #62: refresh the pinned Agent for the current hop.
|
|
522
|
+
await closeActiveAgent();
|
|
523
|
+
const hopHost = currentUrl.hostname.replace(/^\[|\]$/g, '');
|
|
524
|
+
activeAgent = await buildPinnedDispatcher(hopHost);
|
|
400
525
|
response = await request(currentUrl.toString(), {
|
|
401
526
|
method: 'GET',
|
|
527
|
+
...(activeAgent ? { dispatcher: activeAgent } : {}),
|
|
402
528
|
headers: {
|
|
403
529
|
'user-agent': USER_AGENT,
|
|
404
530
|
accept: 'text/html,application/xhtml+xml',
|
|
@@ -436,6 +562,7 @@ export async function webFetchTool(input, ctx) {
|
|
|
436
562
|
/* socket already closed — nothing to do */
|
|
437
563
|
}
|
|
438
564
|
}
|
|
565
|
+
await closeActiveAgent();
|
|
439
566
|
return { ok: false, error: `Exceeded ${MAX_REDIRECTS} redirect hops.` };
|
|
440
567
|
}
|
|
441
568
|
// Drain prior body so the socket can be reused.
|
|
@@ -445,9 +572,11 @@ export async function webFetchTool(input, ctx) {
|
|
|
445
572
|
nextUrl = new URL(locStr, currentUrl);
|
|
446
573
|
}
|
|
447
574
|
catch {
|
|
575
|
+
await closeActiveAgent();
|
|
448
576
|
return { ok: false, error: `Invalid redirect target: ${locStr}` };
|
|
449
577
|
}
|
|
450
578
|
if (nextUrl.protocol !== 'http:' && nextUrl.protocol !== 'https:') {
|
|
579
|
+
await closeActiveAgent();
|
|
451
580
|
return {
|
|
452
581
|
ok: false,
|
|
453
582
|
error: `Refusing redirect to unsupported scheme ${nextUrl.protocol}.`,
|
|
@@ -456,6 +585,7 @@ export async function webFetchTool(input, ctx) {
|
|
|
456
585
|
const nextHost = nextUrl.hostname.replace(/^\[|\]$/g, '');
|
|
457
586
|
const guard = await validateHostnameForFetch(nextHost);
|
|
458
587
|
if (guard) {
|
|
588
|
+
await closeActiveAgent();
|
|
459
589
|
return { ok: false, error: `SSRF refused on redirect: ${guard}` };
|
|
460
590
|
}
|
|
461
591
|
currentUrl = nextUrl;
|
|
@@ -465,13 +595,23 @@ export async function webFetchTool(input, ctx) {
|
|
|
465
595
|
}
|
|
466
596
|
}
|
|
467
597
|
catch (error) {
|
|
598
|
+
await closeActiveAgent();
|
|
468
599
|
const message = error instanceof Error ? error.message : String(error);
|
|
600
|
+
// β1b r1: the pinned-dispatcher path throws `ssrf_pinned_address_blocked: …`
|
|
601
|
+
// when the second DNS lookup answered a private IP. Surface that as a
|
|
602
|
+
// first-class SSRF refusal so callers (and specs) can match on it
|
|
603
|
+
// without grovelling through `Fetch failed:` prefixes.
|
|
604
|
+
if (message.startsWith('ssrf_pinned_address_blocked')) {
|
|
605
|
+
return { ok: false, error: `SSRF refused: ${message}` };
|
|
606
|
+
}
|
|
469
607
|
return { ok: false, error: `Fetch failed: ${message}` };
|
|
470
608
|
}
|
|
471
609
|
if (!response) {
|
|
610
|
+
await closeActiveAgent();
|
|
472
611
|
return { ok: false, error: 'No response received.' };
|
|
473
612
|
}
|
|
474
613
|
if (response.statusCode < 200 || response.statusCode >= 300) {
|
|
614
|
+
await closeActiveAgent();
|
|
475
615
|
return { ok: false, error: `HTTP ${response.statusCode} from ${currentUrl.toString()}` };
|
|
476
616
|
}
|
|
477
617
|
// content-length is advisory — never trust it for the size cap, but
|
|
@@ -489,6 +629,7 @@ export async function webFetchTool(input, ctx) {
|
|
|
489
629
|
catch {
|
|
490
630
|
/* ignore */
|
|
491
631
|
}
|
|
632
|
+
await closeActiveAgent();
|
|
492
633
|
return {
|
|
493
634
|
ok: false,
|
|
494
635
|
error: `Declared content-length ${n} exceeds ${MAX_RESPONSE_BYTES} byte cap.`,
|
|
@@ -499,11 +640,14 @@ export async function webFetchTool(input, ctx) {
|
|
|
499
640
|
const contentType = Array.isArray(contentTypeRaw) ? contentTypeRaw[0] : contentTypeRaw;
|
|
500
641
|
const mime = typeof contentType === 'string' ? contentType.split(';')[0]?.trim().toLowerCase() ?? '' : '';
|
|
501
642
|
if (!ALLOWED_CONTENT_TYPES.includes(mime)) {
|
|
643
|
+
await closeActiveAgent();
|
|
502
644
|
return { ok: false, error: `Disallowed content-type ${mime || '(none)'}; only HTML/XHTML/text.` };
|
|
503
645
|
}
|
|
504
646
|
const bodyResult = await readBodyWithCap(response.body, controller);
|
|
505
|
-
if (!bodyResult.ok)
|
|
647
|
+
if (!bodyResult.ok) {
|
|
648
|
+
await closeActiveAgent();
|
|
506
649
|
return bodyResult;
|
|
650
|
+
}
|
|
507
651
|
const html = bodyResult.buffer.toString('utf8');
|
|
508
652
|
// linkedom is the lightweight DOM Readability needs; jsdom would
|
|
509
653
|
// add ~3 MB to the install footprint for the same surface.
|
|
@@ -524,6 +668,7 @@ export async function webFetchTool(input, ctx) {
|
|
|
524
668
|
`Source: ${safeSource}\n\n` +
|
|
525
669
|
`${scrubbedMarkdown}\n` +
|
|
526
670
|
`</untrusted-content-${nonce}>`;
|
|
671
|
+
await closeActiveAgent();
|
|
527
672
|
return {
|
|
528
673
|
ok: true,
|
|
529
674
|
url: currentUrl.toString(),
|