Compare commits
3 Commits
main
...
fix/gaxios
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
df41a200a5 | ||
|
|
56113720eb | ||
|
|
93131d9d9b |
@ -92,6 +92,7 @@ Docs: https://docs.openclaw.ai
|
|||||||
- Plugins/context engines: enforce owner-aware context-engine registration on both loader and public SDK paths so plugins cannot spoof privileged ownership, claim the core `legacy` engine id, or overwrite an existing engine id through direct SDK imports. (#47595) Thanks @vincentkoc.
|
- Plugins/context engines: enforce owner-aware context-engine registration on both loader and public SDK paths so plugins cannot spoof privileged ownership, claim the core `legacy` engine id, or overwrite an existing engine id through direct SDK imports. (#47595) Thanks @vincentkoc.
|
||||||
- Windows/gateway status: accept `schtasks` `Last Result` output as an alias for `Last Run Result`, so running scheduled-task installs no longer show `Runtime: unknown`. (#47844) Thanks @MoerAI.
|
- Windows/gateway status: accept `schtasks` `Last Result` output as an alias for `Last Run Result`, so running scheduled-task installs no longer show `Runtime: unknown`. (#47844) Thanks @MoerAI.
|
||||||
- Control UI/session routing: preserve established external delivery routes when webchat views or sends in externally originated sessions, so subagent completions still return to the original channel instead of the dashboard. (#47797) Thanks @brokemac79.
|
- Control UI/session routing: preserve established external delivery routes when webchat views or sends in externally originated sessions, so subagent completions still return to the original channel instead of the dashboard. (#47797) Thanks @brokemac79.
|
||||||
|
- Google auth/Node 25: patch `gaxios` to use native fetch without injecting `globalThis.window`, while translating proxy and mTLS transport settings so Google Vertex and Google Chat auth keep working on Node 25. (#47914) Thanks @pdd-cli.
|
||||||
|
|
||||||
## 2026.3.13
|
## 2026.3.13
|
||||||
|
|
||||||
|
|||||||
@ -41,6 +41,9 @@ if (
|
|||||||
) {
|
) {
|
||||||
// Imported as a dependency — skip all entry-point side effects.
|
// Imported as a dependency — skip all entry-point side effects.
|
||||||
} else {
|
} else {
|
||||||
|
const { installGaxiosFetchCompat } = await import("./infra/gaxios-fetch-compat.js");
|
||||||
|
|
||||||
|
installGaxiosFetchCompat();
|
||||||
process.title = "openclaw";
|
process.title = "openclaw";
|
||||||
ensureOpenClawExecMarkerOnProcess();
|
ensureOpenClawExecMarkerOnProcess();
|
||||||
installProcessWarningFilter();
|
installProcessWarningFilter();
|
||||||
|
|||||||
@ -34,6 +34,20 @@ describe("legacy root entry", () => {
|
|||||||
expect(runtimeMocks.runCli).not.toHaveBeenCalled();
|
expect(runtimeMocks.runCli).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("keeps library imports free of global window shims", async () => {
|
||||||
|
const originalWindowDescriptor = Object.getOwnPropertyDescriptor(globalThis, "window");
|
||||||
|
Reflect.deleteProperty(globalThis as object, "window");
|
||||||
|
|
||||||
|
try {
|
||||||
|
await import("./index.js");
|
||||||
|
expect("window" in globalThis).toBe(false);
|
||||||
|
} finally {
|
||||||
|
if (originalWindowDescriptor) {
|
||||||
|
Object.defineProperty(globalThis, "window", originalWindowDescriptor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it("delegates legacy direct-entry execution to run-main", async () => {
|
it("delegates legacy direct-entry execution to run-main", async () => {
|
||||||
const mod = await import("./index.js");
|
const mod = await import("./index.js");
|
||||||
const argv = ["node", "dist/index.js", "status"];
|
const argv = ["node", "dist/index.js", "status"];
|
||||||
|
|||||||
@ -32,7 +32,12 @@ export const waitForever = library.waitForever;
|
|||||||
|
|
||||||
// Legacy direct file entrypoint only. Package root exports now live in library.ts.
|
// Legacy direct file entrypoint only. Package root exports now live in library.ts.
|
||||||
export async function runLegacyCliEntry(argv: string[] = process.argv): Promise<void> {
|
export async function runLegacyCliEntry(argv: string[] = process.argv): Promise<void> {
|
||||||
const { runCli } = await import("./cli/run-main.js");
|
const [{ installGaxiosFetchCompat }, { runCli }] = await Promise.all([
|
||||||
|
import("./infra/gaxios-fetch-compat.js"),
|
||||||
|
import("./cli/run-main.js"),
|
||||||
|
]);
|
||||||
|
|
||||||
|
installGaxiosFetchCompat();
|
||||||
await runCli(argv);
|
await runCli(argv);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
60
src/infra/gaxios-fetch-compat.test.ts
Normal file
60
src/infra/gaxios-fetch-compat.test.ts
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import { HttpsProxyAgent } from "https-proxy-agent";
|
||||||
|
import { ProxyAgent } from "undici";
|
||||||
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
describe("gaxios fetch compat", () => {
|
||||||
|
afterEach(() => {
|
||||||
|
vi.resetModules();
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
vi.unstubAllGlobals();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses native fetch without defining window or importing node-fetch", async () => {
|
||||||
|
const fetchMock = vi.fn<typeof fetch>(async () => {
|
||||||
|
return new Response("ok", {
|
||||||
|
headers: { "content-type": "text/plain" },
|
||||||
|
status: 200,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.stubGlobal("fetch", fetchMock);
|
||||||
|
vi.doMock("node-fetch", () => {
|
||||||
|
throw new Error("node-fetch should not load");
|
||||||
|
});
|
||||||
|
|
||||||
|
const { installGaxiosFetchCompat } = await import("./gaxios-fetch-compat.js");
|
||||||
|
const { Gaxios } = await import("gaxios");
|
||||||
|
|
||||||
|
installGaxiosFetchCompat();
|
||||||
|
|
||||||
|
const res = await new Gaxios().request({
|
||||||
|
responseType: "text",
|
||||||
|
url: "https://example.com",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.data).toBe("ok");
|
||||||
|
expect(fetchMock).toHaveBeenCalledOnce();
|
||||||
|
expect("window" in globalThis).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("translates proxy agents into undici dispatchers for native fetch", async () => {
|
||||||
|
const fetchMock = vi.fn<typeof fetch>(async () => {
|
||||||
|
return new Response("ok", {
|
||||||
|
headers: { "content-type": "text/plain" },
|
||||||
|
status: 200,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const { createGaxiosCompatFetch } = await import("./gaxios-fetch-compat.js");
|
||||||
|
|
||||||
|
const compatFetch = createGaxiosCompatFetch(fetchMock);
|
||||||
|
await compatFetch("https://example.com", {
|
||||||
|
agent: new HttpsProxyAgent("http://proxy.example:8080"),
|
||||||
|
} as RequestInit);
|
||||||
|
|
||||||
|
expect(fetchMock).toHaveBeenCalledOnce();
|
||||||
|
const [, init] = fetchMock.mock.calls[0] ?? [];
|
||||||
|
|
||||||
|
expect(init).not.toHaveProperty("agent");
|
||||||
|
expect((init as { dispatcher?: unknown })?.dispatcher).toBeInstanceOf(ProxyAgent);
|
||||||
|
});
|
||||||
|
});
|
||||||
212
src/infra/gaxios-fetch-compat.ts
Normal file
212
src/infra/gaxios-fetch-compat.ts
Normal file
@ -0,0 +1,212 @@
|
|||||||
|
import type { ConnectionOptions } from "node:tls";
|
||||||
|
import { Gaxios } from "gaxios";
|
||||||
|
import type { Dispatcher } from "undici";
|
||||||
|
import { Agent as UndiciAgent, ProxyAgent } from "undici";
|
||||||
|
|
||||||
|
type ProxyRule = RegExp | URL | string;
|
||||||
|
type TlsCert = ConnectionOptions["cert"];
|
||||||
|
type TlsKey = ConnectionOptions["key"];
|
||||||
|
|
||||||
|
type GaxiosFetchRequestInit = RequestInit & {
|
||||||
|
agent?: unknown;
|
||||||
|
cert?: TlsCert;
|
||||||
|
dispatcher?: Dispatcher;
|
||||||
|
fetchImplementation?: typeof fetch;
|
||||||
|
key?: TlsKey;
|
||||||
|
noProxy?: ProxyRule[];
|
||||||
|
proxy?: string | URL;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ProxyAgentLike = {
|
||||||
|
connectOpts?: { cert?: TlsCert; key?: TlsKey };
|
||||||
|
proxy: URL;
|
||||||
|
};
|
||||||
|
|
||||||
|
type TlsAgentLike = {
|
||||||
|
options?: { cert?: TlsCert; key?: TlsKey };
|
||||||
|
};
|
||||||
|
|
||||||
|
type GaxiosPrototype = {
|
||||||
|
_defaultAdapter: (this: Gaxios, config: GaxiosFetchRequestInit) => Promise<unknown>;
|
||||||
|
};
|
||||||
|
|
||||||
|
let installState: "not-installed" | "installed" = "not-installed";
|
||||||
|
|
||||||
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||||
|
return typeof value === "object" && value !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasDispatcher(value: unknown): value is Dispatcher {
|
||||||
|
return isRecord(value) && typeof value.dispatch === "function";
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasProxyAgentShape(value: unknown): value is ProxyAgentLike {
|
||||||
|
return isRecord(value) && value.proxy instanceof URL;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasTlsAgentShape(value: unknown): value is TlsAgentLike {
|
||||||
|
return isRecord(value) && isRecord(value.options);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveTlsOptions(
|
||||||
|
init: GaxiosFetchRequestInit,
|
||||||
|
url: URL,
|
||||||
|
): { cert?: TlsCert; key?: TlsKey } {
|
||||||
|
const explicit = {
|
||||||
|
cert: init.cert,
|
||||||
|
key: init.key,
|
||||||
|
};
|
||||||
|
if (explicit.cert !== undefined || explicit.key !== undefined) {
|
||||||
|
return explicit;
|
||||||
|
}
|
||||||
|
|
||||||
|
const agent = typeof init.agent === "function" ? init.agent(url) : init.agent;
|
||||||
|
if (hasProxyAgentShape(agent)) {
|
||||||
|
return {
|
||||||
|
cert: agent.connectOpts?.cert,
|
||||||
|
key: agent.connectOpts?.key,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (hasTlsAgentShape(agent)) {
|
||||||
|
return {
|
||||||
|
cert: agent.options?.cert,
|
||||||
|
key: agent.options?.key,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
function urlMayUseProxy(url: URL, noProxy: ProxyRule[] = []): boolean {
|
||||||
|
const rules = [...noProxy];
|
||||||
|
const envRules = (process.env.NO_PROXY ?? process.env.no_proxy)?.split(",") ?? [];
|
||||||
|
for (const rule of envRules) {
|
||||||
|
const trimmed = rule.trim();
|
||||||
|
if (trimmed.length > 0) {
|
||||||
|
rules.push(trimmed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const rule of rules) {
|
||||||
|
if (rule instanceof RegExp) {
|
||||||
|
if (rule.test(url.toString())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (rule instanceof URL) {
|
||||||
|
if (rule.origin === url.origin) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (rule.startsWith("*.") || rule.startsWith(".")) {
|
||||||
|
const cleanedRule = rule.replace(/^\*\./, ".");
|
||||||
|
if (url.hostname.endsWith(cleanedRule)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (rule === url.origin || rule === url.hostname || rule === url.href) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveProxyUri(init: GaxiosFetchRequestInit, url: URL): string | undefined {
|
||||||
|
if (init.proxy) {
|
||||||
|
const proxyUri = String(init.proxy);
|
||||||
|
return urlMayUseProxy(url, init.noProxy) ? proxyUri : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const envProxy =
|
||||||
|
process.env.HTTPS_PROXY ??
|
||||||
|
process.env.https_proxy ??
|
||||||
|
process.env.HTTP_PROXY ??
|
||||||
|
process.env.http_proxy;
|
||||||
|
if (!envProxy) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return urlMayUseProxy(url, init.noProxy) ? envProxy : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildDispatcher(init: GaxiosFetchRequestInit, url: URL): Dispatcher | undefined {
|
||||||
|
if (init.dispatcher) {
|
||||||
|
return init.dispatcher;
|
||||||
|
}
|
||||||
|
|
||||||
|
const agent = typeof init.agent === "function" ? init.agent(url) : init.agent;
|
||||||
|
if (hasDispatcher(agent)) {
|
||||||
|
return agent;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { cert, key } = resolveTlsOptions(init, url);
|
||||||
|
const proxyUri =
|
||||||
|
resolveProxyUri(init, url) ?? (hasProxyAgentShape(agent) ? String(agent.proxy) : undefined);
|
||||||
|
if (proxyUri) {
|
||||||
|
return new ProxyAgent({
|
||||||
|
requestTls: cert !== undefined || key !== undefined ? { cert, key } : undefined,
|
||||||
|
uri: proxyUri,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cert !== undefined || key !== undefined) {
|
||||||
|
return new UndiciAgent({
|
||||||
|
connect: { cert, key },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createGaxiosCompatFetch(baseFetch: typeof fetch = globalThis.fetch): typeof fetch {
|
||||||
|
return async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
|
||||||
|
const gaxiosInit = (init ?? {}) as GaxiosFetchRequestInit;
|
||||||
|
const requestUrl =
|
||||||
|
input instanceof Request
|
||||||
|
? new URL(input.url)
|
||||||
|
: new URL(typeof input === "string" ? input : input.toString());
|
||||||
|
const dispatcher = buildDispatcher(gaxiosInit, requestUrl);
|
||||||
|
|
||||||
|
const nextInit: RequestInit = { ...gaxiosInit };
|
||||||
|
delete (nextInit as GaxiosFetchRequestInit).agent;
|
||||||
|
delete (nextInit as GaxiosFetchRequestInit).cert;
|
||||||
|
delete (nextInit as GaxiosFetchRequestInit).fetchImplementation;
|
||||||
|
delete (nextInit as GaxiosFetchRequestInit).key;
|
||||||
|
delete (nextInit as GaxiosFetchRequestInit).noProxy;
|
||||||
|
delete (nextInit as GaxiosFetchRequestInit).proxy;
|
||||||
|
|
||||||
|
if (dispatcher) {
|
||||||
|
(nextInit as RequestInit & { dispatcher: Dispatcher }).dispatcher = dispatcher;
|
||||||
|
}
|
||||||
|
|
||||||
|
return baseFetch(input, nextInit);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function installGaxiosFetchCompat(): void {
|
||||||
|
if (installState === "installed" || typeof globalThis.fetch !== "function") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const prototype = Gaxios.prototype as unknown as GaxiosPrototype;
|
||||||
|
const originalDefaultAdapter = prototype._defaultAdapter;
|
||||||
|
const compatFetch = createGaxiosCompatFetch();
|
||||||
|
|
||||||
|
prototype._defaultAdapter = function patchedDefaultAdapter(
|
||||||
|
this: Gaxios,
|
||||||
|
config: GaxiosFetchRequestInit,
|
||||||
|
): Promise<unknown> {
|
||||||
|
if (config.fetchImplementation) {
|
||||||
|
return originalDefaultAdapter.call(this, config);
|
||||||
|
}
|
||||||
|
return originalDefaultAdapter.call(this, {
|
||||||
|
...config,
|
||||||
|
fetchImplementation: compatFetch,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
installState = "installed";
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user