feat: honor HTTP_PROXY/HTTPS_PROXY/NO_PROXY for all outbound traffic (#487)

* feat: honor HTTP_PROXY/HTTPS_PROXY/NO_PROXY for all outbound traffic

Install a global undici dispatcher at CLI startup so every in-process fetch
(LLM APIs, MCP HTTP, web tools, telemetry, sign-in, update checks) honors the
standard proxy variables, and propagate NODE_USE_ENV_PROXY to spawned stdio
MCP child processes. Loopback hosts always bypass the proxy; an invalid proxy
URL is reported and ignored rather than aborting startup.

* feat: support SOCKS proxies via ALL_PROXY

Recognize SOCKS proxies (socks5/socks5h/socks4/socks alias) from ALL_PROXY or a
socks-scheme HTTP(S)_PROXY, routing traffic through a custom undici connector
backed by the socks client (reusing undici's own TLS handling for https).
HTTP(S) proxies keep precedence; NO_PROXY and loopback are honored for the SOCKS
path too. Child stdio MCP node processes honor HTTP(S) proxies via
NODE_USE_ENV_PROXY; SOCKS applies to the main process only.

* fix: address proxy review comments (env masking, child NO_PROXY, nix hash)

- Resolve HTTP(S)_PROXY explicitly via the first non-blank casing so a blank
  lowercase var can no longer mask a populated uppercase one (the dispatcher
  installed but went direct), and coerce a SOCKS-scheme value sitting in an
  HTTP(S) var to '' so it is never handed to EnvHttpProxyAgent.
- Reconcile a child's NO_PROXY override across both casings using the first
  non-blank value run through resolveNoProxy, so a per-server config override
  is not shadowed by the injected lowercase value, keeps the loopback bypass,
  and passes '*' through verbatim.
- Update flake.nix pnpmDeps hash for the added socks/undici dependencies.

* fix(proxy): honor http ALL_PROXY, match port-qualified NO_PROXY, note child Node version

- Honor an http-scheme ALL_PROXY as the catch-all fallback for both http and
  https (scheme-specific HTTP(S)_PROXY still wins), so an ALL_PROXY-only setup
  no longer installs a no-op dispatcher and connects direct.
- Make the SOCKS-path NO_PROXY matcher port-aware: a `host:port` entry now
  matches only that port (with IPv6-safe parsing for `::1` / `[::1]:443`).
- Document that child stdio MCP proxying via NODE_USE_ENV_PROXY only applies on
  Node versions that support it (>= 22.21 / >= 24.5).

* fix(proxy): IPv6 + wildcard NO_PROXY and per-server child proxy edges

- Strip IPv6 brackets from a SOCKS proxy host (e.g. ALL_PROXY=socks5://[::1]:1080)
  so the socks client connects to the bare address.
- Add the bracketed [::1] to the loopback bypass: undici's EnvHttpProxyAgent
  only exempts IPv6 loopback when the NO_PROXY entry is bracketed (it mis-parses
  bare ::1). The SOCKS-path matcher normalizes brackets on both sides.
- Match *.domain wildcard (and host:port) NO_PROXY entries in the SOCKS matcher.
- Compute the child stdio proxy env from the MERGED env so a proxy declared only
  in a server's config.env also enables NODE_USE_ENV_PROXY.

* fix(proxy): synthesize HTTP(S)_PROXY from ALL_PROXY for child processes

proxyEnvForChild now hands spawned stdio MCP children the resolved
HTTP_PROXY/HTTPS_PROXY (in both casings), synthesizing them from an http-scheme
ALL_PROXY when no scheme-specific variable is set. Node's --use-env-proxy reads
HTTP_PROXY/HTTPS_PROXY (not ALL_PROXY), so an ALL_PROXY-only parent now proxies
the child consistently with the main process. Shared resolveHttpProxyUrls helper
is reused by createProxyDispatcher and proxyEnvForChild.

* chore(changeset): tighten proxy changeset wording
This commit is contained in:
Kai 2026-06-06 01:44:54 +08:00 committed by GitHub
parent d995928681
commit 4d113949c8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 932 additions and 6 deletions

View file

@ -0,0 +1,7 @@
---
"@moonshot-ai/agent-core": minor
"@moonshot-ai/kimi-code-sdk": minor
"@moonshot-ai/kimi-code": minor
---
Honor the standard `HTTP_PROXY` / `HTTPS_PROXY` / `ALL_PROXY` / `NO_PROXY` environment variables, including SOCKS proxies, for all outbound traffic.

View file

@ -8,6 +8,7 @@
import {
createKimiHarness,
flushDiagnosticLogs,
installGlobalProxyDispatcher,
log,
resolveGlobalLogPath,
resolveKimiHome,
@ -117,6 +118,10 @@ const MIGRATE_CLI_OPTIONS: CLIOptions = {
export function main(): void {
process.title = PROCESS_NAME;
installCrashHandlers();
// Route all outbound fetch through HTTP_PROXY/HTTPS_PROXY (honoring NO_PROXY)
// before any client is constructed. No-op when no proxy variable is set; an
// invalid proxy URL is reported and ignored rather than aborting startup.
installGlobalProxyDispatcher();
installNativeModuleHook();
if (runNativeAssetSmokeIfRequested()) return;

View file

@ -163,6 +163,21 @@ The CLI also reads several standard system variables to detect the runtime envir
- `WSL_DISTRO_NAME`, `WSLENV`: detect WSL for the clipboard PowerShell bridge
- `LOCALAPPDATA`: used on Windows when probing for the Git Bash installation path
## HTTP proxy
Kimi Code honors the standard proxy environment variables for all outbound traffic — model API calls, MCP servers, web tools, telemetry, sign-in, and update checks:
- `HTTP_PROXY` / `http_proxy`: proxy for `http://` requests
- `HTTPS_PROXY` / `https_proxy`: proxy for `https://` requests
- `ALL_PROXY` / `all_proxy`: fallback proxy used when the scheme-specific variable is unset; this is where a SOCKS proxy is usually set
- `NO_PROXY` / `no_proxy`: comma-separated hosts that bypass the proxy
Both HTTP(S) and SOCKS proxies are supported. A SOCKS proxy is recognized by its scheme — `socks5://`, `socks5h://`, `socks4://`, or `socks://` (an alias for `socks5://`) — and is typically set via `ALL_PROXY` (the form used by tools like Clash and V2RayN). An HTTP(S) proxy takes precedence over `ALL_PROXY` for HTTP/HTTPS traffic.
The proxy is applied only when one of these variables is set; otherwise connections are made directly. Loopback hosts (`localhost`, `127.0.0.1`, `::1`) always bypass the proxy, so a local server such as a localhost MCP server keeps working when a proxy is configured — add your own internal hosts to `NO_PROXY` to exempt them too.
Stdio MCP servers that run as Node child processes honor `HTTP_PROXY` / `HTTPS_PROXY` / `NO_PROXY` automatically when the child's Node version supports `NODE_USE_ENV_PROXY` (Node ≥ 22.21 or ≥ 24.5); SOCKS proxying applies to Kimi Code's own traffic only.
## Next steps
- [Config overrides](./overrides.md) — how environment variables, CLI options, and the config file interact by priority

View file

@ -163,6 +163,21 @@ CLI 还会读取一些标准系统变量来检测运行环境,不会修改它
- `WSL_DISTRO_NAME``WSLENV`:检测 WSL用于剪贴板 PowerShell 桥接
- `LOCALAPPDATA`Windows 上探测 Git Bash 安装路径
## HTTP 代理
Kimi Code 会遵循标准代理环境变量,让所有出网流量——模型 API 调用、MCP 服务、网络工具、遥测、登录、更新检查——都走代理:
- `HTTP_PROXY` / `http_proxy`:用于 `http://` 请求的代理
- `HTTPS_PROXY` / `https_proxy`:用于 `https://` 请求的代理
- `ALL_PROXY` / `all_proxy`:当对应 scheme 的变量未设置时使用的兜底代理SOCKS 代理通常设在这里
- `NO_PROXY` / `no_proxy`:以逗号分隔的、绕过代理的主机列表
同时支持 HTTP(S) 代理和 SOCKS 代理。SOCKS 代理通过 scheme 识别——`socks5://``socks5h://``socks4://``socks://``socks5://` 的别名)——通常设在 `ALL_PROXY`Clash、V2RayN 等工具使用的形式)。对 HTTP/HTTPS 流量HTTP(S) 代理优先于 `ALL_PROXY`
仅当设置了其中任一变量时才启用代理,否则直连。回环地址(`localhost``127.0.0.1``::1`)始终绕过代理,因此配置了代理后,本地服务(例如 localhost 上的 MCP 服务)仍能正常工作——你也可以把自己的内网主机加入 `NO_PROXY` 一并放行。
以 Node 子进程运行的 stdio MCP 服务,在其 Node 版本支持 `NODE_USE_ENV_PROXY`Node ≥ 22.21 或 ≥ 24.5)会自动遵循 `HTTP_PROXY` / `HTTPS_PROXY` / `NO_PROXY`SOCKS 代理仅作用于 Kimi Code 自身的流量。
## 下一步
- [配置覆盖](./overrides.md) — 环境变量、CLI 选项、配置文件的优先级关系

View file

@ -140,7 +140,7 @@
inherit (finalAttrs) pname version src pnpmWorkspaces;
inherit pnpm;
fetcherVersion = 3;
hash = "sha256-/Kgq76JAgi1NygbnYkBNACUl+U9TO5zwF1MaCzk3n9o=";
hash = "sha256-x5O/+bDRoEW3juoxe3XyvTJxHitQ0hZ2nVXBItkBA5E=";
};
nativeBuildInputs = [

View file

@ -71,7 +71,9 @@
"regexp.escape": "^2.0.1",
"retry": "0.13.1",
"smol-toml": "^1.6.1",
"socks": "^2.8.9",
"tar": "^7.5.13",
"undici": "^7.27.1",
"yauzl": "^3.3.0",
"zod": "catalog:"
},

View file

@ -16,6 +16,7 @@ export {
} from './logging/logger';
export { resolveLoggingConfig } from './logging/resolve-config';
export type { ResolveLoggingInput } from './logging/resolve-config';
export { installGlobalProxyDispatcher } from './utils/proxy';
export type {
LogContext,
LogEntry,

View file

@ -1,5 +1,6 @@
import { ErrorCodes, KimiError } from '#/errors';
import type { McpServerStdioConfig } from '#/config/schema';
import { proxyEnvForChild, reconcileChildNoProxy } from '#/utils/proxy';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
@ -216,13 +217,24 @@ class BoundedTail {
}
// Inherit the parent's env so PATH/HOME/etc. survive — otherwise `npx`/`uvx`
// style stdio servers fail to launch even with a valid config. Explicit
// `config.env` entries still override on conflict.
function mergeStdioEnv(configEnv?: Record<string, string>): Record<string, string> {
// style stdio servers fail to launch even with a valid config. `config.env`
// overrides on conflict. A node child does not inherit our in-process undici
// dispatcher, so `proxyEnvForChild` adds `NODE_USE_ENV_PROXY` (and a
// loopback-protected `NO_PROXY`) to make it honor the proxy natively (on a Node
// version that supports the flag — ≥22.21 or ≥24.5). It is computed from the
// MERGED env so a proxy declared only in `config.env` is honored too.
// `reconcileChildNoProxy` then mirrors a single-casing `NO_PROXY` override onto
// both casings so it isn't shadowed by the injected value.
export function mergeStdioEnv(
configEnv?: Record<string, string>,
parentEnv: Readonly<Record<string, string | undefined>> = process.env,
): Record<string, string> {
const merged: Record<string, string> = {};
for (const [key, value] of Object.entries(process.env)) {
for (const [key, value] of Object.entries(parentEnv)) {
if (value !== undefined) merged[key] = value;
}
if (configEnv !== undefined) Object.assign(merged, configEnv);
Object.assign(merged, proxyEnvForChild(merged));
reconcileChildNoProxy(merged, configEnv);
return merged;
}

View file

@ -0,0 +1,378 @@
import {
Agent,
buildConnector,
type Dispatcher,
EnvHttpProxyAgent,
setGlobalDispatcher as undiciSetGlobalDispatcher,
} from 'undici';
import { SocksClient } from 'socks';
type Env = Readonly<Record<string, string | undefined>>;
/** A parsed SOCKS proxy endpoint, in the shape the `socks` client expects. */
export interface SocksProxyConfig {
/** SOCKS protocol version: 4 (socks4/socks4a) or 5 (socks/socks5/socks5h). */
readonly type: 4 | 5;
readonly host: string;
readonly port: number;
readonly userId?: string;
readonly password?: string;
}
// Loopback hosts always bypass the proxy. Neither undici's EnvHttpProxyAgent,
// Node's `--use-env-proxy`, nor our SOCKS connector exempt loopback by default,
// so without this a user with a proxy set would route `http://localhost:PORT`
// traffic (e.g. a local MCP server) through the proxy — a confusing failure
// that only proxy users would hit.
// `::1` and the bracketed `[::1]` are both listed: undici's EnvHttpProxyAgent
// only bypasses the IPv6 loopback when the NO_PROXY entry is bracketed (it
// otherwise mis-parses `::1` as host `:` port `1`), while our own SOCKS matcher
// normalizes brackets away — so including both covers every path.
const LOOPBACK_NO_PROXY = ['localhost', '127.0.0.1', '::1', '[::1]'] as const;
const SOCKS_SCHEMES = new Set(['socks', 'socks4', 'socks4a', 'socks5', 'socks5h']);
/** Lowercase URL scheme (without the trailing colon), or undefined if absent. */
function schemeOf(value: string): string | undefined {
return /^([a-z][a-z0-9+.-]*):/i.exec(value)?.[1]?.toLowerCase();
}
/** First non-blank value among `keys` (both casings are passed in by callers). */
function firstNonBlank(env: Env, keys: readonly string[]): string | undefined {
for (const key of keys) {
const value = env[key]?.trim();
if (value !== undefined && value.length > 0) return value;
}
return undefined;
}
/** The value if it is an HTTP/HTTPS-scheme proxy (not SOCKS), else undefined. */
function httpSchemeValue(value: string | undefined): string | undefined {
return value !== undefined && !SOCKS_SCHEMES.has(schemeOf(value) ?? '') ? value : undefined;
}
/**
* True when an HTTP/HTTPS-scheme proxy is configured via `HTTP_PROXY`,
* `HTTPS_PROXY`, or an http-scheme `ALL_PROXY` (the catch-all fallback).
*/
function hasHttpProxy(env: Env): boolean {
return [
firstNonBlank(env, ['http_proxy', 'HTTP_PROXY']),
firstNonBlank(env, ['https_proxy', 'HTTPS_PROXY']),
firstNonBlank(env, ['all_proxy', 'ALL_PROXY']),
].some((value) => httpSchemeValue(value) !== undefined);
}
/**
* Resolve the effective http/https proxy URLs: the scheme-specific
* `HTTP_PROXY`/`HTTPS_PROXY` (ignoring a SOCKS-scheme value), falling back to an
* http-scheme `ALL_PROXY` catch-all. `undefined` for a scheme with no usable
* value.
*/
function resolveHttpProxyUrls(env: Env): { httpProxy?: string; httpsProxy?: string } {
const allProxy = httpSchemeValue(firstNonBlank(env, ['all_proxy', 'ALL_PROXY']));
return {
httpProxy: httpSchemeValue(firstNonBlank(env, ['http_proxy', 'HTTP_PROXY'])) ?? allProxy,
httpsProxy: httpSchemeValue(firstNonBlank(env, ['https_proxy', 'HTTPS_PROXY'])) ?? allProxy,
};
}
/**
* Resolve a SOCKS proxy from the environment, or `undefined` if none. A SOCKS
* proxy may be declared via `ALL_PROXY` (the common form for Clash / V2RayN) or
* by putting a `socks*` scheme in `HTTP(S)_PROXY`. `ALL_PROXY` wins, then
* `HTTPS_PROXY`, then `HTTP_PROXY`. `socks://` is an alias for `socks5://`.
*/
export function resolveSocksProxy(env: Env = process.env): SocksProxyConfig | undefined {
const candidates = [
firstNonBlank(env, ['all_proxy', 'ALL_PROXY']),
firstNonBlank(env, ['https_proxy', 'HTTPS_PROXY']),
firstNonBlank(env, ['http_proxy', 'HTTP_PROXY']),
];
for (const value of candidates) {
if (value === undefined) continue;
const scheme = schemeOf(value);
if (scheme === undefined || !SOCKS_SCHEMES.has(scheme)) continue;
let url: URL;
try {
url = new URL(value);
} catch {
continue;
}
const config: SocksProxyConfig = {
type: scheme === 'socks4' || scheme === 'socks4a' ? 4 : 5,
// Strip IPv6 brackets: the `socks` client wants the bare address (`::1`),
// not the URL's bracketed `[::1]`, which it would treat as a hostname.
host: url.hostname.replaceAll(/^\[|\]$/g, ''),
port: url.port ? Number(url.port) : 1080,
...(url.username ? { userId: decodeURIComponent(url.username) } : {}),
...(url.password ? { password: decodeURIComponent(url.password) } : {}),
};
return config;
}
return undefined;
}
/** True when any HTTP(S) or SOCKS proxy variable is set to a usable value. */
export function isProxyConfigured(env: Env = process.env): boolean {
return hasHttpProxy(env) || resolveSocksProxy(env) !== undefined;
}
/**
* The effective `NO_PROXY` with loopback hosts guaranteed present so local
* traffic stays direct. Reads both casings (lowercase first when non-blank,
* matching undici), preserves the user's entries, and appends only the missing
* loopback hosts.
*
* The `*` wildcard ("bypass everything") is returned verbatim: undici only
* honors it as an exact-string match, so appending loopback would silently
* defeat the user's explicit opt-out and route all non-loopback traffic
* through the proxy.
*/
export function resolveNoProxy(env: Env = process.env): string {
// Prefer the first non-blank casing; an empty `no_proxy=''` must not mask a
// populated `NO_PROXY` (`??` would, since `''` is not nullish).
const raw = [env['no_proxy'], env['NO_PROXY']].find((value) => (value?.trim() ?? '').length > 0) ?? '';
const hosts = raw
.split(',')
.map((host) => host.trim())
.filter((host) => host.length > 0);
if (hosts.includes('*')) return '*';
for (const loopback of LOOPBACK_NO_PROXY) {
if (!hosts.includes(loopback)) hosts.push(loopback);
}
return hosts.join(',');
}
/**
* Build a predicate that returns true when a host (and optional port) should
* bypass the proxy, given a `NO_PROXY` string. Matches `*` (all), exact hosts,
* and subdomains for both bare (`example.com`) and leading-dot (`.example.com`)
* entries; a port-qualified entry (`host:443`) matches only that port. Used for
* the SOCKS path, where bypass is not handled by undici for us.
*/
export function makeNoProxyMatcher(noProxy: string): (host: string, port?: number | string) => boolean {
const entries = noProxy
.split(',')
.map((entry) => entry.trim().toLowerCase())
.filter((entry) => entry.length > 0);
if (entries.includes('*')) return () => true;
const parsed = entries.map(parseNoProxyEntry);
return (host: string, port?: number | string) => {
const target = host.toLowerCase().replaceAll(/^\[|\]$/g, '');
const targetPort = port === undefined ? undefined : String(port);
return parsed.some(
({ host: entry, port: entryPort }) =>
(entryPort === undefined || entryPort === targetPort) &&
(target === entry || target.endsWith(`.${entry}`)),
);
};
}
/**
* Split a `NO_PROXY` entry into host (leading `.` stripped) and optional port.
* Handles bracketed IPv6 (`[::1]:443`) and avoids mistaking a bare IPv6
* address's colons (`::1`) for a `host:port` separator.
*/
function parseNoProxyEntry(entry: string): { host: string; port?: string } {
let host = entry;
let port: string | undefined;
if (entry.startsWith('[')) {
const close = entry.indexOf(']');
host = entry.slice(1, close);
const rest = entry.slice(close + 1);
if (rest.startsWith(':')) port = rest.slice(1);
} else {
const colon = entry.indexOf(':');
// Only a single colon followed by digits is a port; multiple colons mean a
// bare IPv6 address (e.g. `::1`), which carries no port.
if (colon !== -1 && colon === entry.lastIndexOf(':') && /^\d+$/.test(entry.slice(colon + 1))) {
host = entry.slice(0, colon);
port = entry.slice(colon + 1);
}
}
// Normalize a wildcard domain (`*.example.com`) and a leading-dot
// (`.example.com`) to the bare domain; subdomain matching is handled below.
if (host.startsWith('*.')) host = host.slice(2);
else if (host.startsWith('.')) host = host.slice(1);
return port === undefined ? { host } : { host, port };
}
export interface ProxyAgentFactories {
/** Build the dispatcher for an HTTP/HTTPS proxy. */
readonly makeHttpAgent: (options: {
httpProxy?: string;
httpsProxy?: string;
noProxy: string;
}) => Dispatcher;
/** Build the dispatcher for a SOCKS proxy. */
readonly makeSocksAgent: (options: { proxy: SocksProxyConfig; noProxy: string }) => Dispatcher;
}
const defaultMakeHttpAgent: ProxyAgentFactories['makeHttpAgent'] = ({ httpProxy, httpsProxy, noProxy }) =>
// Pass the resolved proxy URLs explicitly: left to itself EnvHttpProxyAgent
// reads `http_proxy ?? HTTP_PROXY`, where a blank lowercase value would mask a
// populated uppercase one and silently disable proxying. noProxy is likewise
// pre-resolved to guarantee the loopback bypass.
new EnvHttpProxyAgent({ httpProxy, httpsProxy, noProxy });
const defaultMakeSocksAgent: ProxyAgentFactories['makeSocksAgent'] = ({ proxy, noProxy }) => {
// undici has no SOCKS support, so we drive a custom connector: tunnel the
// destination through the SOCKS proxy with the `socks` client, then hand the
// established socket back to undici's connector — which performs the TLS
// upgrade for https targets (reusing undici's ALPN/servername handling).
const directConnect = buildConnector({});
const bypass = makeNoProxyMatcher(noProxy);
const connect: typeof directConnect = (options, callback) => {
if (bypass(options.hostname, options.port)) {
directConnect(options, callback);
return;
}
void (async () => {
try {
const isTls = options.protocol === 'https:';
const port = Number(options.port) || (isTls ? 443 : 80);
const { socket } = await SocksClient.createConnection({
proxy: { host: proxy.host, port: proxy.port, type: proxy.type, userId: proxy.userId, password: proxy.password },
command: 'connect',
destination: { host: options.hostname, port },
});
if (isTls) {
// Upgrade the SOCKS socket to TLS via undici's own connector.
directConnect({ ...options, httpSocket: socket } as Parameters<typeof directConnect>[0], callback);
} else {
socket.setNoDelay(true);
callback(null, socket);
}
} catch (error) {
callback(error instanceof Error ? error : new Error(String(error)), null);
}
})();
};
return new Agent({ connect });
};
/**
* Build an undici dispatcher that routes outbound `fetch` through the
* configured proxy while honoring the (loopback-augmented) `NO_PROXY`. An
* HTTP/HTTPS proxy takes precedence for matching traffic; otherwise a SOCKS
* proxy (`ALL_PROXY` or a `socks*` scheme) is used. Returns `undefined` when no
* proxy variable is set, so the zero-config majority keeps Node's default
* dispatcher untouched.
*/
export function createProxyDispatcher(
env: Env = process.env,
factories: Partial<ProxyAgentFactories> = {},
): Dispatcher | undefined {
const { makeHttpAgent = defaultMakeHttpAgent, makeSocksAgent = defaultMakeSocksAgent } = factories;
try {
if (hasHttpProxy(env)) {
// Coerce a missing value to '' (falsy to undici) so EnvHttpProxyAgent
// neither builds a broken agent from a socks: URI nor re-reads a
// blank-masked value from env.
const { httpProxy, httpsProxy } = resolveHttpProxyUrls(env);
return makeHttpAgent({
httpProxy: httpProxy ?? '',
httpsProxy: httpsProxy ?? '',
noProxy: resolveNoProxy(env),
});
}
const socks = resolveSocksProxy(env);
if (socks !== undefined) {
return makeSocksAgent({ proxy: socks, noProxy: resolveNoProxy(env) });
}
return undefined;
} catch (error) {
// A malformed proxy URL makes agent construction throw synchronously. Don't
// abort startup with a raw stack trace — report it and fall back to direct.
const reason = error instanceof Error ? error.message : String(error);
process.stderr.write(`kimi: ignoring invalid proxy configuration (${reason}); connecting directly\n`);
return undefined;
}
}
export interface InstallProxyDeps {
readonly setGlobalDispatcher: (dispatcher: Dispatcher) => void;
readonly createProxyDispatcher: (env: Env) => Dispatcher | undefined;
}
const defaultInstallProxyDeps: InstallProxyDeps = {
setGlobalDispatcher: undiciSetGlobalDispatcher,
createProxyDispatcher,
};
/**
* Install the proxy dispatcher as the process-wide undici dispatcher so every
* `fetch` LLM SDKs, in-process MCP HTTP, telemetry, OAuth, web tools, update
* checks, downloads honors the proxy. Call once at process startup, before
* any network use. No-op (returns `false`) when no proxy variable is set.
*/
export function installGlobalProxyDispatcher(
env: Env = process.env,
deps: InstallProxyDeps = defaultInstallProxyDeps,
): boolean {
const dispatcher = deps.createProxyDispatcher(env);
if (dispatcher === undefined) return false;
deps.setGlobalDispatcher(dispatcher);
return true;
}
/**
* Environment additions for spawned child node processes (e.g. stdio MCP
* servers) so they honor the proxy natively via Node's `--use-env-proxy`
* without bundling undici. An in-process global dispatcher is NOT inherited
* across a process boundary only env vars are so children rely on this.
*
* Only applies to HTTP/HTTPS proxies: Node's `--use-env-proxy` does not support
* SOCKS, so a SOCKS-only proxy yields `{}` (child SOCKS proxying is out of
* scope). Everything is set in BOTH casings: the child inherits the parent's
* env and undici reads the lowercase form first, so the lowercase variants must
* also carry the resolved values or the protection/proxying is silently lost.
*
* Because `--use-env-proxy` reads `HTTP_PROXY`/`HTTPS_PROXY` (not `ALL_PROXY`),
* an http-scheme `ALL_PROXY` is synthesized into the scheme-specific variables
* so an `ALL_PROXY`-only parent still proxies the child.
*/
export function proxyEnvForChild(env: Env = process.env): Record<string, string> {
if (!hasHttpProxy(env)) return {};
const noProxy = resolveNoProxy(env);
const result: Record<string, string> = {
NODE_USE_ENV_PROXY: '1',
NO_PROXY: noProxy,
no_proxy: noProxy,
};
const { httpProxy, httpsProxy } = resolveHttpProxyUrls(env);
if (httpProxy !== undefined) {
result['HTTP_PROXY'] = httpProxy;
result['http_proxy'] = httpProxy;
}
if (httpsProxy !== undefined) {
result['HTTPS_PROXY'] = httpsProxy;
result['https_proxy'] = httpsProxy;
}
return result;
}
/**
* Mirror a server config's `NO_PROXY` override onto both casings of the child
* env. undici reads the lowercase `no_proxy` first, so without this the value
* {@link proxyEnvForChild} injected in the other casing would shadow an
* explicit per-server override.
*
* Uses the first NON-blank casing (a blank `no_proxy=''` must not mask a
* populated `NO_PROXY`, mirroring {@link resolveNoProxy}) and runs the value
* back through {@link resolveNoProxy} so the loopback bypass is preserved and
* `*` passes through verbatim. No-op when config sets no usable `NO_PROXY`.
*/
export function reconcileChildNoProxy(
childEnv: Record<string, string>,
configEnv?: Record<string, string>,
): void {
const override = [configEnv?.['no_proxy'], configEnv?.['NO_PROXY']].find(
(value) => (value?.trim() ?? '').length > 0,
);
if (override === undefined) return;
const noProxy = resolveNoProxy({ no_proxy: override, NO_PROXY: override });
childEnv['NO_PROXY'] = noProxy;
childEnv['no_proxy'] = noProxy;
}

View file

@ -4,7 +4,7 @@ import { fileURLToPath } from 'node:url';
import { describe, expect, it } from 'vitest';
import { KimiError } from '../../src/errors';
import { StdioMcpClient } from '../../src/mcp/client-stdio';
import { mergeStdioEnv, StdioMcpClient } from '../../src/mcp/client-stdio';
const here = import.meta.dirname;
const fixture = join(here, 'fixtures', 'mock-stdio-server.mjs');
@ -246,3 +246,24 @@ describe('StdioMcpClient', () => {
expect(closes).toEqual([]);
}, 15000);
});
describe('mergeStdioEnv', () => {
it('enables NODE_USE_ENV_PROXY for a proxy set only in the server config.env', () => {
const merged = mergeStdioEnv({ HTTP_PROXY: 'http://corp:3128' }, { PATH: '/usr/bin' });
expect(merged['HTTP_PROXY']).toBe('http://corp:3128');
expect(merged['NODE_USE_ENV_PROXY']).toBe('1');
expect(merged['NO_PROXY']).toBe('localhost,127.0.0.1,::1,[::1]');
expect(merged['PATH']).toBe('/usr/bin');
});
it('does not inject NODE_USE_ENV_PROXY when no proxy is configured', () => {
const merged = mergeStdioEnv(undefined, { PATH: '/usr/bin' });
expect(merged['NODE_USE_ENV_PROXY']).toBeUndefined();
expect(merged['PATH']).toBe('/usr/bin');
});
it('lets config.env override the parent env', () => {
const merged = mergeStdioEnv({ FOO: 'override' }, { FOO: 'parent', PATH: '/x' });
expect(merged['FOO']).toBe('override');
});
});

View file

@ -0,0 +1,433 @@
import { describe, expect, it, vi } from 'vitest';
import {
createProxyDispatcher,
installGlobalProxyDispatcher,
isProxyConfigured,
makeNoProxyMatcher,
proxyEnvForChild,
reconcileChildNoProxy,
resolveNoProxy,
resolveSocksProxy,
} from '../../src/utils/proxy';
describe('isProxyConfigured', () => {
it('is false when no proxy variable is set', () => {
expect(isProxyConfigured({})).toBe(false);
});
it('is true for HTTP_PROXY and the lowercase form', () => {
expect(isProxyConfigured({ HTTP_PROXY: 'http://p:3128' })).toBe(true);
expect(isProxyConfigured({ http_proxy: 'http://p:3128' })).toBe(true);
});
it('is true for HTTPS_PROXY', () => {
expect(isProxyConfigured({ HTTPS_PROXY: 'http://p:3128' })).toBe(true);
});
it('ignores blank values', () => {
expect(isProxyConfigured({ HTTP_PROXY: ' ' })).toBe(false);
});
it('is true when only a SOCKS proxy (ALL_PROXY) is set', () => {
expect(isProxyConfigured({ ALL_PROXY: 'socks5://127.0.0.1:1080' })).toBe(true);
});
it('is true for an http-scheme ALL_PROXY', () => {
expect(isProxyConfigured({ ALL_PROXY: 'http://proxy:8080' })).toBe(true);
});
});
describe('resolveNoProxy', () => {
it('adds loopback hosts when NO_PROXY is unset', () => {
expect(resolveNoProxy({})).toBe('localhost,127.0.0.1,::1,[::1]');
});
it('preserves existing hosts and appends only the missing loopback hosts', () => {
expect(resolveNoProxy({ NO_PROXY: 'example.com, 127.0.0.1' })).toBe(
'example.com,127.0.0.1,localhost,::1,[::1]',
);
});
it('reads the lowercase no_proxy', () => {
expect(resolveNoProxy({ no_proxy: 'internal' })).toBe('internal,localhost,127.0.0.1,::1,[::1]');
});
it('preserves the "*" wildcard verbatim (it must stay an exact match to bypass everything)', () => {
expect(resolveNoProxy({ NO_PROXY: '*' })).toBe('*');
expect(resolveNoProxy({ NO_PROXY: 'corp, *' })).toBe('*');
});
it('falls through to NO_PROXY when no_proxy is set but blank', () => {
expect(resolveNoProxy({ no_proxy: '', NO_PROXY: 'corp' })).toBe('corp,localhost,127.0.0.1,::1,[::1]');
});
});
describe('resolveSocksProxy', () => {
it('returns undefined when no SOCKS proxy is configured', () => {
expect(resolveSocksProxy({})).toBeUndefined();
expect(resolveSocksProxy({ HTTP_PROXY: 'http://p:3128' })).toBeUndefined();
});
it('parses ALL_PROXY socks5 and defaults the port to 1080', () => {
expect(resolveSocksProxy({ ALL_PROXY: 'socks5://10.0.0.1' })).toEqual({
type: 5,
host: '10.0.0.1',
port: 1080,
});
});
it('normalizes the socks:// alias to socks5', () => {
expect(resolveSocksProxy({ ALL_PROXY: 'socks://127.0.0.1:7890' })).toEqual({
type: 5,
host: '127.0.0.1',
port: 7890,
});
});
it('parses socks4 as type 4', () => {
expect(resolveSocksProxy({ ALL_PROXY: 'socks4://127.0.0.1:1080' })).toEqual({
type: 4,
host: '127.0.0.1',
port: 1080,
});
});
it('reads credentials from the URL', () => {
expect(resolveSocksProxy({ ALL_PROXY: 'socks5://user:pass@127.0.0.1:1080' })).toEqual({
type: 5,
host: '127.0.0.1',
port: 1080,
userId: 'user',
password: 'pass',
});
});
it('picks up a SOCKS scheme set in HTTP_PROXY', () => {
expect(resolveSocksProxy({ HTTP_PROXY: 'socks5://127.0.0.1:1080' })).toEqual({
type: 5,
host: '127.0.0.1',
port: 1080,
});
});
it('prefers ALL_PROXY over a SOCKS value in HTTPS_PROXY', () => {
expect(resolveSocksProxy({ ALL_PROXY: 'socks5://a:1', HTTPS_PROXY: 'socks5://b:2' })).toEqual({
type: 5,
host: 'a',
port: 1,
});
});
it('is case-insensitive on the scheme', () => {
expect(resolveSocksProxy({ ALL_PROXY: 'SOCKS5://127.0.0.1:1080' })).toEqual({
type: 5,
host: '127.0.0.1',
port: 1080,
});
});
it('strips IPv6 brackets from the SOCKS proxy host', () => {
expect(resolveSocksProxy({ ALL_PROXY: 'socks5://[::1]:1080' })).toEqual({
type: 5,
host: '::1',
port: 1080,
});
});
});
describe('makeNoProxyMatcher', () => {
it('bypasses everything for the "*" wildcard', () => {
const bypass = makeNoProxyMatcher('*');
expect(bypass('example.com')).toBe(true);
expect(bypass('127.0.0.1')).toBe(true);
});
it('bypasses listed hosts and loopback, not others', () => {
const bypass = makeNoProxyMatcher('localhost,127.0.0.1,::1,corp.internal');
expect(bypass('localhost')).toBe(true);
expect(bypass('127.0.0.1')).toBe(true);
expect(bypass('corp.internal')).toBe(true);
expect(bypass('example.com')).toBe(false);
});
it('matches subdomains for both bare and leading-dot entries', () => {
const bypass = makeNoProxyMatcher('.example.com,foo.org');
expect(bypass('a.example.com')).toBe(true);
expect(bypass('example.com')).toBe(true);
expect(bypass('sub.foo.org')).toBe(true);
expect(bypass('foo.org')).toBe(true);
expect(bypass('other.com')).toBe(false);
});
it('is case-insensitive', () => {
expect(makeNoProxyMatcher('Corp.Internal')('corp.INTERNAL')).toBe(true);
});
it('never bypasses when NO_PROXY is empty', () => {
expect(makeNoProxyMatcher('')('example.com')).toBe(false);
});
it('matches a port-qualified entry only for the matching port', () => {
const bypass = makeNoProxyMatcher('api.example.com:443');
expect(bypass('api.example.com', 443)).toBe(true);
expect(bypass('api.example.com', '443')).toBe(true);
expect(bypass('api.example.com', 80)).toBe(false);
});
it('still matches a bare IPv6 loopback entry (colons are not a port)', () => {
const bypass = makeNoProxyMatcher('::1');
expect(bypass('::1')).toBe(true);
});
it('matches a bracketed IPv6 target against a bare ::1 entry', () => {
expect(makeNoProxyMatcher('::1')('[::1]')).toBe(true);
});
it('matches a *.domain wildcard entry against subdomains and the apex', () => {
const bypass = makeNoProxyMatcher('*.example.com');
expect(bypass('api.example.com')).toBe(true);
expect(bypass('example.com')).toBe(true);
expect(bypass('other.com')).toBe(false);
});
});
describe('createProxyDispatcher', () => {
it('returns undefined and builds nothing when no proxy is set', () => {
const makeHttpAgent = vi.fn();
const makeSocksAgent = vi.fn();
expect(createProxyDispatcher({}, { makeHttpAgent, makeSocksAgent })).toBeUndefined();
expect(makeHttpAgent).not.toHaveBeenCalled();
expect(makeSocksAgent).not.toHaveBeenCalled();
});
it('builds an HTTP-proxy agent with resolved proxy URLs and loopback-protected NO_PROXY', () => {
const sentinel = { id: 'http' } as never;
const makeHttpAgent = vi.fn().mockReturnValue(sentinel);
const makeSocksAgent = vi.fn();
const result = createProxyDispatcher(
{ HTTP_PROXY: 'http://p:3128', NO_PROXY: 'corp' },
{ makeHttpAgent, makeSocksAgent },
);
expect(result).toBe(sentinel);
expect(makeHttpAgent).toHaveBeenCalledWith(
expect.objectContaining({ httpProxy: 'http://p:3128', noProxy: 'corp,localhost,127.0.0.1,::1,[::1]' }),
);
expect(makeSocksAgent).not.toHaveBeenCalled();
});
it('passes the non-blank HTTP_PROXY even when the lowercase form is an empty string', () => {
// undici's EnvHttpProxyAgent reads `http_proxy ?? HTTP_PROXY`, so a blank
// lowercase value would mask the uppercase one — we must resolve and pass
// the proxy URL explicitly, otherwise the dispatcher installs but goes direct.
const makeHttpAgent = vi.fn().mockReturnValue({ id: 'http' } as never);
createProxyDispatcher({ http_proxy: '', HTTP_PROXY: 'http://proxy:3128' }, { makeHttpAgent });
expect(makeHttpAgent).toHaveBeenCalledWith(
expect.objectContaining({ httpProxy: 'http://proxy:3128' }),
);
});
it('suppresses a SOCKS value sitting in HTTP_PROXY rather than feeding it to the HTTP agent', () => {
// EnvHttpProxyAgent cannot do SOCKS; passing it a socks: URI builds a broken
// ProxyAgent. When HTTPS is a real http-proxy (so the HTTP path is taken),
// the socks-in-HTTP_PROXY value must be coerced away, not forwarded.
const makeHttpAgent = vi.fn().mockReturnValue({ id: 'http' } as never);
const makeSocksAgent = vi.fn();
createProxyDispatcher(
{ HTTP_PROXY: 'socks5://h:1', HTTPS_PROXY: 'http://real:3128' },
{ makeHttpAgent, makeSocksAgent },
);
expect(makeHttpAgent).toHaveBeenCalledWith(
expect.objectContaining({ httpProxy: '', httpsProxy: 'http://real:3128' }),
);
expect(makeSocksAgent).not.toHaveBeenCalled();
});
it('uses an http-scheme ALL_PROXY as the fallback for both http and https', () => {
const makeHttpAgent = vi.fn().mockReturnValue({ id: 'http' } as never);
const makeSocksAgent = vi.fn();
createProxyDispatcher({ ALL_PROXY: 'http://proxy:8080' }, { makeHttpAgent, makeSocksAgent });
expect(makeHttpAgent).toHaveBeenCalledWith(
expect.objectContaining({ httpProxy: 'http://proxy:8080', httpsProxy: 'http://proxy:8080' }),
);
expect(makeSocksAgent).not.toHaveBeenCalled();
});
it('prefers a scheme-specific proxy over an http ALL_PROXY fallback', () => {
const makeHttpAgent = vi.fn().mockReturnValue({ id: 'http' } as never);
createProxyDispatcher(
{ HTTP_PROXY: 'http://specific:1', ALL_PROXY: 'http://all:2' },
{ makeHttpAgent },
);
expect(makeHttpAgent).toHaveBeenCalledWith(
expect.objectContaining({ httpProxy: 'http://specific:1', httpsProxy: 'http://all:2' }),
);
});
it('builds a SOCKS agent when only a SOCKS proxy is configured', () => {
const sentinel = { id: 'socks' } as never;
const makeSocksAgent = vi.fn().mockReturnValue(sentinel);
const makeHttpAgent = vi.fn();
const result = createProxyDispatcher(
{ ALL_PROXY: 'socks5://127.0.0.1:1080', NO_PROXY: 'corp' },
{ makeHttpAgent, makeSocksAgent },
);
expect(result).toBe(sentinel);
expect(makeSocksAgent).toHaveBeenCalledWith({
proxy: { type: 5, host: '127.0.0.1', port: 1080 },
noProxy: 'corp,localhost,127.0.0.1,::1,[::1]',
});
expect(makeHttpAgent).not.toHaveBeenCalled();
});
it('prefers an HTTP(S) proxy over a SOCKS ALL_PROXY', () => {
const makeHttpAgent = vi.fn().mockReturnValue({ id: 'http' } as never);
const makeSocksAgent = vi.fn();
createProxyDispatcher(
{ HTTP_PROXY: 'http://p:3128', ALL_PROXY: 'socks5://127.0.0.1:1080' },
{ makeHttpAgent, makeSocksAgent },
);
expect(makeHttpAgent).toHaveBeenCalledTimes(1);
expect(makeSocksAgent).not.toHaveBeenCalled();
});
it('reports and ignores an invalid proxy configuration instead of crashing', () => {
const stderr = vi.spyOn(process.stderr, 'write').mockReturnValue(true);
const makeHttpAgent = vi.fn(() => {
throw new TypeError('Invalid URL');
});
try {
expect(createProxyDispatcher({ HTTP_PROXY: 'gibberish' }, { makeHttpAgent })).toBeUndefined();
expect(makeHttpAgent).toHaveBeenCalledTimes(1);
expect(stderr).toHaveBeenCalled();
} finally {
stderr.mockRestore();
}
});
});
describe('installGlobalProxyDispatcher', () => {
it('installs the dispatcher exactly once and returns true when a proxy is set', () => {
const dispatcher = { id: 'dispatcher' } as never;
const setGlobalDispatcher = vi.fn();
const createDispatcher = vi.fn().mockReturnValue(dispatcher);
const installed = installGlobalProxyDispatcher(
{ HTTP_PROXY: 'http://p:3128' },
{ setGlobalDispatcher, createProxyDispatcher: createDispatcher },
);
expect(installed).toBe(true);
expect(setGlobalDispatcher).toHaveBeenCalledTimes(1);
expect(setGlobalDispatcher).toHaveBeenCalledWith(dispatcher);
});
it('installs nothing and returns false when no proxy is set', () => {
const setGlobalDispatcher = vi.fn();
const createDispatcher = vi.fn().mockReturnValue(undefined);
const installed = installGlobalProxyDispatcher(
{},
{ setGlobalDispatcher, createProxyDispatcher: createDispatcher },
);
expect(installed).toBe(false);
expect(setGlobalDispatcher).not.toHaveBeenCalled();
});
});
describe('proxyEnvForChild', () => {
it('returns an empty object when no proxy is configured', () => {
expect(proxyEnvForChild({})).toEqual({});
});
it('enables Node native env-proxy, mirrors the proxy URL, and protects loopback', () => {
// Sets BOTH casings: a child inherits the parent's env, and undici reads
// the lowercase form first — so both NO_PROXY and the proxy URL must carry
// the resolved value or the protection/proxying is silently defeated.
expect(proxyEnvForChild({ HTTP_PROXY: 'http://p:3128', NO_PROXY: 'corp' })).toEqual({
NODE_USE_ENV_PROXY: '1',
NO_PROXY: 'corp,localhost,127.0.0.1,::1,[::1]',
no_proxy: 'corp,localhost,127.0.0.1,::1,[::1]',
HTTP_PROXY: 'http://p:3128',
http_proxy: 'http://p:3128',
});
});
it('synthesizes scheme-specific proxies from an http-scheme ALL_PROXY for the child', () => {
// Node's --use-env-proxy reads HTTP_PROXY/HTTPS_PROXY, not ALL_PROXY, so an
// ALL_PROXY-only parent must hand the child the scheme-specific vars.
expect(proxyEnvForChild({ ALL_PROXY: 'http://proxy:8080' })).toEqual({
NODE_USE_ENV_PROXY: '1',
NO_PROXY: 'localhost,127.0.0.1,::1,[::1]',
no_proxy: 'localhost,127.0.0.1,::1,[::1]',
HTTP_PROXY: 'http://proxy:8080',
http_proxy: 'http://proxy:8080',
HTTPS_PROXY: 'http://proxy:8080',
https_proxy: 'http://proxy:8080',
});
});
it('passes the "*" wildcard through to the child verbatim in both casings', () => {
expect(proxyEnvForChild({ HTTP_PROXY: 'http://p:3128', NO_PROXY: '*' })).toEqual({
NODE_USE_ENV_PROXY: '1',
NO_PROXY: '*',
no_proxy: '*',
HTTP_PROXY: 'http://p:3128',
http_proxy: 'http://p:3128',
});
});
it('returns an empty object for a SOCKS-only proxy (children cannot use SOCKS natively)', () => {
expect(proxyEnvForChild({ ALL_PROXY: 'socks5://127.0.0.1:1080' })).toEqual({});
});
});
describe('reconcileChildNoProxy', () => {
it('mirrors a config NO_PROXY override onto both casings and re-adds loopback', () => {
// Without mirroring, the lowercase `no_proxy` injected by proxyEnvForChild
// would shadow the server config's uppercase override (undici reads
// lowercase first); the override must also keep the loopback bypass.
const childEnv: Record<string, string> = {
NO_PROXY: 'corp,localhost,127.0.0.1,::1,[::1]',
no_proxy: 'corp,localhost,127.0.0.1,::1,[::1]',
};
reconcileChildNoProxy(childEnv, { NO_PROXY: 'server.local' });
expect(childEnv['NO_PROXY']).toBe('server.local,localhost,127.0.0.1,::1,[::1]');
expect(childEnv['no_proxy']).toBe('server.local,localhost,127.0.0.1,::1,[::1]');
});
it('prefers the first non-blank casing (lowercase) and keeps loopback', () => {
const childEnv: Record<string, string> = { NO_PROXY: 'aug', no_proxy: 'aug' };
reconcileChildNoProxy(childEnv, { no_proxy: 'lower', NO_PROXY: 'upper' });
expect(childEnv['NO_PROXY']).toBe('lower,localhost,127.0.0.1,::1,[::1]');
expect(childEnv['no_proxy']).toBe('lower,localhost,127.0.0.1,::1,[::1]');
});
it('does not let a blank lowercase no_proxy mask a populated NO_PROXY', () => {
const childEnv: Record<string, string> = { NO_PROXY: 'aug', no_proxy: 'aug' };
reconcileChildNoProxy(childEnv, { no_proxy: '', NO_PROXY: 'real.corp' });
expect(childEnv['NO_PROXY']).toBe('real.corp,localhost,127.0.0.1,::1,[::1]');
expect(childEnv['no_proxy']).toBe('real.corp,localhost,127.0.0.1,::1,[::1]');
});
it('passes the "*" wildcard override through verbatim', () => {
const childEnv: Record<string, string> = {
NO_PROXY: 'corp,localhost,127.0.0.1,::1,[::1]',
no_proxy: 'corp,localhost,127.0.0.1,::1,[::1]',
};
reconcileChildNoProxy(childEnv, { NO_PROXY: '*' });
expect(childEnv['NO_PROXY']).toBe('*');
expect(childEnv['no_proxy']).toBe('*');
});
it('ignores a config that provides no NO_PROXY or only a blank one', () => {
const childEnv: Record<string, string> = { NO_PROXY: 'aug', no_proxy: 'aug' };
reconcileChildNoProxy(childEnv, { OTHER: 'x' });
expect(childEnv['no_proxy']).toBe('aug');
reconcileChildNoProxy(childEnv, { no_proxy: '' });
expect(childEnv['no_proxy']).toBe('aug');
});
it('is a no-op when there is no config', () => {
const childEnv: Record<string, string> = { no_proxy: 'aug' };
reconcileChildNoProxy(childEnv, undefined);
expect(childEnv['no_proxy']).toBe('aug');
});
});

View file

@ -62,6 +62,10 @@ export {
} from '@moonshot-ai/agent-core';
export type { LogContext, LogLevel, LogPayload, Logger } from '@moonshot-ai/agent-core';
// Process-wide HTTP proxy bootstrap — installed once at CLI startup so all
// outbound fetch honors HTTP_PROXY / HTTPS_PROXY / NO_PROXY.
export { installGlobalProxyDispatcher } from '@moonshot-ai/agent-core';
// Goal completion message builder — single source of truth for the deterministic
// "Goal complete · turns · tokens · time" text (live render + persisted message).
export { buildGoalCompletionMessage } from '@moonshot-ai/agent-core';

33
pnpm-lock.yaml generated
View file

@ -277,9 +277,15 @@ importers:
smol-toml:
specifier: ^1.6.1
version: 1.6.1
socks:
specifier: ^2.8.9
version: 2.8.9
tar:
specifier: ^7.5.13
version: 7.5.13
undici:
specifier: ^7.27.1
version: 7.27.1
yauzl:
specifier: ^3.3.0
version: 3.3.0
@ -3646,6 +3652,10 @@ packages:
resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==}
engines: {node: '>= 12'}
ip-address@10.2.0:
resolution: {integrity: sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==}
engines: {node: '>= 12'}
ipaddr.js@1.9.1:
resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==}
engines: {node: '>= 0.10'}
@ -4857,10 +4867,18 @@ packages:
resolution: {integrity: sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==}
engines: {node: '>=20'}
smart-buffer@4.2.0:
resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==}
engines: {node: '>= 6.0.0', npm: '>= 3.0.0'}
smol-toml@1.6.1:
resolution: {integrity: sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg==}
engines: {node: '>= 18'}
socks@2.8.9:
resolution: {integrity: sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==}
engines: {node: '>= 10.0.0', npm: '>= 3.0.0'}
source-map-js@1.2.1:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'}
@ -5147,6 +5165,10 @@ packages:
undici-types@6.21.0:
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
undici@7.27.1:
resolution: {integrity: sha512-UDdpiex+mzigiyrXrGbiUaF4HzTNhKbh2vRNFaTMzcqmLIPrZxaCtwo/1TMSuWoM1Xz3WiTo9KdgI3kRqYzJGg==}
engines: {node: '>=20.18.1'}
unicode-emoji-modifier-base@1.0.0:
resolution: {integrity: sha512-yLSH4py7oFH3oG/9K+XWrz1pSi3dfUrWEnInbxMfArOfc1+33BlGPQtLsOYwvdMy11AwUBetYuaRxSPqgkq+8g==}
engines: {node: '>=4'}
@ -8653,6 +8675,8 @@ snapshots:
ip-address@10.1.0: {}
ip-address@10.2.0: {}
ipaddr.js@1.9.1: {}
is-array-buffer@3.0.5:
@ -10066,8 +10090,15 @@ snapshots:
ansi-styles: 6.2.3
is-fullwidth-code-point: 5.1.0
smart-buffer@4.2.0: {}
smol-toml@1.6.1: {}
socks@2.8.9:
dependencies:
ip-address: 10.2.0
smart-buffer: 4.2.0
source-map-js@1.2.1: {}
source-map@0.6.1: {}
@ -10346,6 +10377,8 @@ snapshots:
undici-types@6.21.0: {}
undici@7.27.1: {}
unicode-emoji-modifier-base@1.0.0: {}
unified@11.0.5: