Compare commits

...

3 Commits

6 changed files with 296 additions and 1 deletions

View File

@ -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

View File

@ -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();

View File

@ -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"];

View File

@ -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);
} }

View 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);
});
});

View 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";
}