diff --git a/CHANGELOG.md b/CHANGELOG.md index c45c8b7ba7db..0c713d6df924 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,6 +51,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- **Browser actions on Node 24:** keep browser request cancellation bound to the client and response lifetime instead of Node 24.16+'s prematurely aborted body-stream signal, preventing valid POST actions from failing after JSON parsing. Thanks @obviyus and @vincentkoc. - **SecretRef model credentials:** keep resolved provider secrets behind process-local sentinels through auth storage, stream setup, SDK configuration, and managed local-provider probing, then inject plaintext only at the final network or provider-plugin boundary while retaining exact-value log redaction. (#102008, #102009) - **Lean local model shell access:** keep `exec` directly visible beside the default structured Tool Search controls so coding-tuned local models can use their shell fallback instead of searching for missing domain tools. (#87587) Thanks @vincentkoc and @maweibin. - **OAuth refresh contention diagnostics:** keep local lock paths out of user-facing refresh failures and avoid duplicate failure prefixes while preserving structured provider and profile classification. (#83383) Thanks @vincentkoc. @@ -75,7 +76,7 @@ Docs: https://docs.openclaw.ai - **Microsoft Teams attachment metadata:** bound Bot Framework `attachmentInfo` JSON reads and cancel oversized streams before they can exhaust Gateway memory. (#99125) Thanks @ly85206559. - **Agent auth copy order:** preserve the source agent's portable auth-profile precedence when copying credentials to a new agent while excluding skipped profiles and transient auth state. (#100833) Thanks @machine3at. - **Memory session repair:** keep daily dreaming ingestion bookkeeping outside session-corpus audit and repair so `memory status --fix` preserves healthy daily state. (#93389) Thanks @Alix-007 and @vincentkoc. -- **Remote browser CDP policy:** allow the configured CDP control host through an existing hostname allowlist without widening page navigation policy, while keeping strict-policy discovery bound to the configured control authority. (#100986, #100819) Thanks @NianJiuZst and @SarinV. +- **Remote browser CDP policy:** allow the configured CDP control host through an existing hostname allowlist without widening page navigation policy, while keeping strict-policy discovery bound to the configured control authority. (#100986, #100819) Thanks @NianJiuZst, @SarinV, and @vincentkoc. - **Config unset diagnostics:** explain when an inherited or default configuration value cannot be unset instead of reporting a misleading successful deletion. (#96557) Thanks @moeghashim. - **Crestodian command probes:** contain stdout and stderr stream failures while keeping child-process close and spawn errors authoritative, preventing unhandled probe crashes. (#100741) Thanks @lsr911. - **Feishu mention forwarding:** fail closed when the bot Open ID is unavailable so group messages cannot be misclassified as explicit bot mentions. (#100891) Thanks @zhangguiping-xydt. @@ -202,7 +203,7 @@ Docs: https://docs.openclaw.ai ### Complete contribution record -This audited record covers the complete v2026.6.11..b81666ca6af25c86cc099983a4358cdc5ea9ced8 history: 1974 merged PRs. The generation manifest also supplies direct commits as editorial input; the grouped notes above prioritize user impact. +This audited record covers the complete v2026.6.11..HEAD history: 1974 merged PRs. The generation manifest also supplies direct commits as editorial input; the grouped notes above prioritize user impact. #### Pull requests diff --git a/extensions/browser/src/browser/server-middleware.test.ts b/extensions/browser/src/browser/server-middleware.test.ts new file mode 100644 index 000000000000..6b60f4d7554a --- /dev/null +++ b/extensions/browser/src/browser/server-middleware.test.ts @@ -0,0 +1,51 @@ +import { EventEmitter } from "node:events"; +import type { Express, NextFunction, Request, Response } from "express"; +import { describe, expect, it, vi } from "vitest"; +import { installBrowserCommonMiddleware } from "./server-middleware.js"; + +type Middleware = (req: Request, res: Response, next: NextFunction) => void; + +describe("installBrowserCommonMiddleware", () => { + it("shadows native request signals with the browser response-lifetime signal", () => { + const middleware: Middleware[] = []; + const app = { + use: vi.fn((...handlers: unknown[]) => { + for (const handler of handlers) { + if (typeof handler === "function") { + middleware.push(handler as Middleware); + } + } + return app; + }), + } as unknown as Express; + installBrowserCommonMiddleware(app); + + const nativeController = new AbortController(); + const req = new EventEmitter() as EventEmitter & Request; + const requestPrototype = Object.create(Object.getPrototypeOf(req)) as object; + Object.defineProperty(requestPrototype, "signal", { + configurable: true, + get: () => nativeController.signal, + }); + Object.setPrototypeOf(req, requestPrototype); + + const res = new EventEmitter() as EventEmitter & Response; + Object.defineProperty(res, "writableEnded", { value: false, writable: true }); + const next = vi.fn(); + const commonMiddleware = middleware[0]; + if (!commonMiddleware) { + throw new Error("browser common middleware was not installed"); + } + + commonMiddleware(req, res, next); + + expect(next).toHaveBeenCalledOnce(); + expect(Object.hasOwn(req, "signal")).toBe(true); + expect(req.signal).not.toBe(nativeController.signal); + expect(req.signal.aborted).toBe(false); + + req.emit("aborted"); + expect(req.signal.aborted).toBe(true); + expect(req.signal.reason).toEqual(new Error("request aborted")); + }); +}); diff --git a/extensions/browser/src/browser/server-middleware.ts b/extensions/browser/src/browser/server-middleware.ts index 4e882ebd707e..bc3eb0afe4fb 100644 --- a/extensions/browser/src/browser/server-middleware.ts +++ b/extensions/browser/src/browser/server-middleware.ts @@ -33,15 +33,12 @@ export function installBrowserCommonMiddleware(app: Express) { abort(); } }); - // Make the signal available to browser route handlers on Node versions - // whose IncomingMessage does not already expose a native read-only signal. - const requestWithSignal = req as Request & { signal?: AbortSignal }; - if (!(requestWithSignal.signal instanceof AbortSignal)) { - Object.defineProperty(req, "signal", { - value: ctrl.signal, - configurable: true, - }); - } + // Node 24.16+'s native request signal aborts when a POST body finishes. + // Browser work follows the client/response lifetime instead. + Object.defineProperty(req, "signal", { + value: ctrl.signal, + configurable: true, + }); next(); }); app.use(express.json({ limit: "1mb" }));