Skip to content

Commit 930e76f

Browse files
committed
Auto-merge upstream openclaw/openclaw
2 parents aa6fb18 + 9006817 commit 930e76f

23 files changed

Lines changed: 790 additions & 14 deletions

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ Docs: https://docs.openclaw.ai
1616
- Doctor/systemd: keep `openclaw doctor --repair` and service reinstall from re-embedding dotenv-backed secrets in user systemd units, while preserving newer inline overrides over stale state-dir `.env` values. (#66249) Thanks @tmimmanuel.
1717
- Doctor/plugins: cache external `preferOver` catalog lookups within each plugin auto-enable pass so large `agents.list` configs no longer peg CPU and repeatedly reread plugin catalogs during doctor/plugins resolution. (#66246) Thanks @yfge.
1818
- Agents/local models: clarify low-context preflight hints for self-hosted models, point config-backed caps at the relevant OpenClaw setting, and stop suggesting larger models when `agents.defaults.contextTokens` is the real limit. (#66236) Thanks @ImLukeF.
19+
- Browser/SSRF: restore hostname navigation under the default browser SSRF policy while keeping explicit strict mode reachable from config, and keep managed loopback CDP `/json/new` fallback requests on the local CDP control policy so browser follow-up fixes stop regressing normal navigation or self-blocking local CDP control. (#66386) Thanks @obviyus.
20+
- Browser/SSRF: preserve explicit strict browser navigation mode for legacy `browser.ssrfPolicy.allowPrivateNetwork: false` configs by normalizing the legacy alias to the canonical strict marker instead of silently widening those installs to the default non-strict hostname-navigation path.
1921

2022
## 2026.4.14-beta.1
2123

@@ -44,15 +46,18 @@ Docs: https://docs.openclaw.ai
4446
- Cron/scheduler: stop inventing short retries when cron next-run calculation returns no valid future slot, and keep a maintenance wake armed so enabled unscheduled jobs recover without entering a refire loop. (#66019, #66083) Thanks @mbelinky.
4547
- Cron/scheduler: preserve the active error-backoff floor when maintenance repair recomputes a missing cron next-run, so recurring errored jobs do not resume early after a transient next-run resolution failure. (#66019, #66083, #66113) Thanks @mbelinky.
4648
- Outbound/delivery-queue: persist the originating outbound `session` context on queued delivery entries and replay it during recovery, so write-ahead-queued sends keep their original outbound media policy context after restart instead of evaluating against a missing session. (#66025) Thanks @eleqtrizit.
49+
- Memory/Ollama: restore the built-in `ollama` embedding adapter in memory-core so explicit `memorySearch.provider: "ollama"` works again, and include endpoint-aware cache keys so different Ollama hosts do not reuse each other's embeddings. (#63429, #66078, #66163) Thanks @nnish16 and @vincentkoc.
4750
- Auto-reply/queue: split collect-mode followup drains into contiguous groups by per-message authorization context (sender id, owner status, exec/bash-elevated overrides), so queued items from different senders or exec configs no longer execute under the last queued run's owner-only and exec-approval context. (#66024) Thanks @eleqtrizit.
4851
- Dreaming/memory-core: require a live queued Dreaming cron event before the heartbeat hook runs the sweep, so managed Dreaming no longer replays on later heartbeats after the scheduled run was already consumed. (#66139) Thanks @mbelinky.
4952
- Control UI/Dreaming: stop Imported Insights and Memory Palace from calling optional `memory-wiki` gateway methods when the plugin is off, and refresh config before wiki reloads so the Dreaming tab stops showing misleading unknown-method failures. (#66140) Thanks @mbelinky.
5053
- Agents/tools: only mark streamed unknown-tool retries as counted when a streamed message actually classifies an unavailable tool, and keep incomplete streamed tool names from resetting the retry streak before the final assistant message arrives. (#66145) Thanks @dutifulbob.
5154
- Memory/active-memory: move recalled memory onto the hidden untrusted prompt-prefix path instead of system prompt injection, label the visible Active Memory status line fields, and include the resolved recall provider/model in gateway debug logs so trace/debug output matches what the model actually saw. (#66144) Thanks @Takhoffman.
5255
- Memory/QMD: stop treating legacy lowercase `memory.md` as a second default root collection, so QMD recall no longer searches phantom `memory-alt-*` collections and builtin/QMD root-memory fallback stays aligned. (#66141) Thanks @mbelinky.
56+
- Agents/subagents: ship `dist/agents/subagent-registry.runtime.js` in npm builds so `runtime: "subagent"` runs stop stalling in `queued` after the registry import fails. (#66189) Thanks @yqli2420 and @vincentkoc.
5357
- Agents/OpenAI: map `minimal` thinking to OpenAI's supported `low` reasoning effort for GPT-5.4 requests, so embedded runs stop failing request validation. Thanks @steipete.
5458
- Voice-call/media-stream: resolve the source IP from trusted forwarding headers for per-IP pending-connection limits when `webhookSecurity.trustForwardingHeaders` and `trustedProxyIPs` are configured, and reserve `maxConnections` capacity for in-flight WebSocket upgrades so concurrent handshakes can no longer momentarily exceed the operator-set cap. (#66027) Thanks @eleqtrizit.
5559
- Feishu/allowlist: canonicalize allowlist entries by explicit `user`/`chat` kind, strip repeated `feishu:`/`lark:` provider prefixes, and stop folding opaque Feishu IDs to lowercase, so allowlist matching no longer crosses user/chat namespaces or widens to case-insensitive ID matches the operator did not intend. (#66021) Thanks @eleqtrizit.
60+
- Telegram/status commands: let read-only status slash commands bypass busy topic turns, while keeping `/export-session` on the normal lane so it cannot interleave with an in-flight session mutation. (#66226) Thanks @VACInc and @vincentkoc.
5661
- TTS/reply media: persist OpenClaw temp voice outputs into managed outbound media and allow them through reply-media normalization, so voice-note replies stop silently dropping. (#63511) Thanks @jetd1.
5762
- Agents/tools: treat Windows drive-letter paths (`C:\\...`) as absolute when resolving sandbox and read-tool paths so workspace root is not prepended under POSIX path rules. (#54039) Thanks @ly85206559 and @vincentkoc.
5863
- Agents/OpenAI: recover embedded GPT-style runs when reasoning-only or empty turns need bounded continuation, with replay-safe retry gating and incomplete-turn fallback when no visible answer arrives. (#66167) thanks @jalehman

extensions/browser/src/browser/config.test.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -318,7 +318,16 @@ describe("browser config", () => {
318318
dangerouslyAllowPrivateNetwork: false,
319319
},
320320
});
321-
expect(resolved.ssrfPolicy).toEqual({});
321+
expect(resolved.ssrfPolicy).toEqual({ dangerouslyAllowPrivateNetwork: false });
322+
});
323+
324+
it("preserves legacy explicit strict mode from allowPrivateNetwork=false", () => {
325+
const resolved = resolveBrowserConfig({
326+
ssrfPolicy: {
327+
allowPrivateNetwork: false,
328+
},
329+
} as unknown as BrowserConfig);
330+
expect(resolved.ssrfPolicy).toEqual({ dangerouslyAllowPrivateNetwork: false });
322331
});
323332

324333
it("keeps allowlist-only browser SSRF policy strict by default", () => {

extensions/browser/src/browser/config.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,11 @@ function resolveBrowserSsrFPolicy(cfg: BrowserConfig | undefined): SsrFPolicy |
149149
}
150150

151151
return {
152-
...(resolvedAllowPrivateNetwork ? { dangerouslyAllowPrivateNetwork: true } : {}),
152+
...(resolvedAllowPrivateNetwork ||
153+
dangerouslyAllowPrivateNetwork === false ||
154+
allowPrivateNetwork === false
155+
? { dangerouslyAllowPrivateNetwork: resolvedAllowPrivateNetwork }
156+
: {}),
153157
...(allowedHostnames ? { allowedHostnames } : {}),
154158
...(hostnameAllowlist ? { hostnameAllowlist } : {}),
155159
};

extensions/browser/src/browser/navigation-guard.test.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,18 @@ describe("browser navigation guard", () => {
128128
expect(lookupFn).not.toHaveBeenCalled();
129129
});
130130

131+
it("allows hostname navigation when the default strict policy object is present", async () => {
132+
const lookupFn = createLookupFn("93.184.216.34");
133+
await expect(
134+
assertBrowserNavigationAllowed({
135+
url: "https://example.com",
136+
lookupFn,
137+
ssrfPolicy: {},
138+
}),
139+
).resolves.toBeUndefined();
140+
expect(lookupFn).toHaveBeenCalledWith("example.com", { all: true });
141+
});
142+
131143
it("allows explicitly allowed hostnames in strict mode", async () => {
132144
const lookupFn = createLookupFn("93.184.216.34");
133145
await expect(
@@ -300,8 +312,11 @@ describe("browser navigation guard", () => {
300312
).resolves.toBeUndefined();
301313
});
302314

303-
it("treats default browser SSRF mode as requiring redirect-hop inspection", () => {
304-
expect(requiresInspectableBrowserNavigationRedirects()).toBe(true);
315+
it("requires redirect-hop inspection only in explicit strict mode", () => {
316+
expect(requiresInspectableBrowserNavigationRedirects()).toBe(false);
317+
expect(
318+
requiresInspectableBrowserNavigationRedirects({ dangerouslyAllowPrivateNetwork: false }),
319+
).toBe(true);
305320
expect(requiresInspectableBrowserNavigationRedirects({ allowPrivateNetwork: true })).toBe(
306321
false,
307322
);

extensions/browser/src/browser/navigation-guard.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ export function withBrowserNavigationPolicy(
4343
}
4444

4545
export function requiresInspectableBrowserNavigationRedirects(ssrfPolicy?: SsrFPolicy): boolean {
46-
return !isPrivateNetworkAllowedByPolicy(ssrfPolicy);
46+
return ssrfPolicy?.dangerouslyAllowPrivateNetwork === false;
4747
}
4848

4949
export function requiresInspectableBrowserNavigationRedirectsForUrl(
@@ -122,6 +122,7 @@ export async function assertBrowserNavigationAllowed(
122122
// the same address that passed policy checks.
123123
if (
124124
opts.ssrfPolicy &&
125+
opts.ssrfPolicy.dangerouslyAllowPrivateNetwork === false &&
125126
!isPrivateNetworkAllowedByPolicy(opts.ssrfPolicy) &&
126127
!isIpLiteralHostname(parsed.hostname) &&
127128
!isExplicitlyAllowedBrowserHostname(parsed.hostname, opts.ssrfPolicy)

extensions/browser/src/browser/routes/tabs.attach-only.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ describe("browser tab routes attachOnly loopback profiles", () => {
4242
{
4343
id: "PAGE-1",
4444
title: "WordPress",
45-
url: "https://example.test/wp-login.php",
45+
url: "https://example.com/wp-login.php",
4646
webSocketDebuggerUrl: "ws://127.0.0.1:9222/devtools/page/PAGE-1",
4747
type: "page",
4848
},
@@ -73,7 +73,7 @@ describe("browser tab routes attachOnly loopback profiles", () => {
7373
{
7474
targetId: "PAGE-1",
7575
title: "WordPress",
76-
url: "",
76+
url: "https://example.com/wp-login.php",
7777
wsUrl: "ws://127.0.0.1:9222/devtools/page/PAGE-1",
7878
type: "page",
7979
},

extensions/browser/src/browser/server-context.remote-profile-tab-ops.fallback.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ describe("browser remote profile fallback and attachOnly behavior", () => {
8080
it("fails closed for remote tab opens in strict mode without Playwright", async () => {
8181
vi.spyOn(deps.pwAiModule, "getPwAiModule").mockResolvedValue(null);
8282
const { state, remote, fetchMock } = deps.createRemoteRouteHarness();
83-
state.resolved.ssrfPolicy = {};
83+
state.resolved.ssrfPolicy = { dangerouslyAllowPrivateNetwork: false };
8484

8585
await expect(remote.openTab("https://example.com")).rejects.toBeInstanceOf(
8686
deps.InvalidBrowserNavigationUrlError,

extensions/browser/src/browser/server-context.tab-ops.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -230,14 +230,14 @@ export function createProfileTabOps({
230230
{
231231
method: "PUT",
232232
},
233-
ssrfPolicyOpts.ssrfPolicy,
233+
getCdpControlPolicy(),
234234
).catch(async (err) => {
235235
if (String(err).includes("HTTP 405")) {
236236
return await fetchJson<CdpTarget>(
237237
endpoint,
238238
CDP_JSON_NEW_TIMEOUT_MS,
239239
undefined,
240-
ssrfPolicyOpts.ssrfPolicy,
240+
getCdpControlPolicy(),
241241
);
242242
}
243243
throw err;

extensions/browser/src/browser/server-context.tab-selection-state.test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ vi.hoisted(() => {
66
});
77

88
import "./server-context.chrome-test-harness.js";
9+
import * as cdpHelpersModule from "./cdp.helpers.js";
910
import * as cdpModule from "./cdp.js";
1011
import { InvalidBrowserNavigationUrlError } from "./navigation-guard.js";
1112
import { createBrowserRouteContext } from "./server-context.js";
@@ -296,4 +297,38 @@ describe("browser server-context tab selection state", () => {
296297
);
297298
expect(fetchMock).not.toHaveBeenCalled();
298299
});
300+
301+
it("uses the loopback CDP control policy for /json/new fallback requests", async () => {
302+
vi.spyOn(cdpModule, "createTargetViaCdp").mockRejectedValue(new Error("cdp unavailable"));
303+
const fetchJson = vi.spyOn(cdpHelpersModule, "fetchJson");
304+
fetchJson.mockRejectedValueOnce(new Error("HTTP 405")).mockResolvedValueOnce({
305+
id: "NEW",
306+
title: "New Tab",
307+
url: "https://example.com",
308+
webSocketDebuggerUrl: "ws://127.0.0.1/devtools/page/NEW",
309+
type: "page",
310+
});
311+
312+
const state = makeState("openclaw");
313+
state.resolved.ssrfPolicy = {};
314+
const ctx = createBrowserRouteContext({ getState: () => state });
315+
const openclaw = ctx.forProfile("openclaw");
316+
317+
const opened = await openclaw.openTab("https://example.com");
318+
expect(opened.targetId).toBe("NEW");
319+
expect(fetchJson).toHaveBeenNthCalledWith(
320+
1,
321+
expect.stringContaining("/json/new"),
322+
expect.any(Number),
323+
{ method: "PUT" },
324+
undefined,
325+
);
326+
expect(fetchJson).toHaveBeenNthCalledWith(
327+
2,
328+
expect.stringContaining("/json/new"),
329+
expect.any(Number),
330+
undefined,
331+
undefined,
332+
);
333+
});
299334
});

extensions/memory-core/src/memory/index.test.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,16 @@ import { resolveSessionTranscriptsDirForAgent } from "openclaw/plugin-sdk/memory
66
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
77
import {
88
clearMemoryEmbeddingProviders as clearRegistry,
9+
listMemoryEmbeddingProviders as listRegisteredAdapters,
910
registerMemoryEmbeddingProvider as registerAdapter,
1011
} from "../../../../src/plugins/memory-embedding-providers.js";
1112
import "./test-runtime-mocks.js";
1213
import type { MemoryIndexManager } from "./index.js";
1314
import { getMemorySearchManager, closeAllMemorySearchManagers } from "./index.js";
14-
import { registerBuiltInMemoryEmbeddingProviders } from "./provider-adapters.js";
15+
import {
16+
DEFAULT_OLLAMA_EMBEDDING_MODEL,
17+
registerBuiltInMemoryEmbeddingProviders,
18+
} from "./provider-adapters.js";
1519

1620
let embedBatchCalls = 0;
1721
let embedBatchInputCalls = 0;
@@ -108,6 +112,18 @@ vi.mock("./embeddings.js", () => {
108112
});
109113

110114
describe("memory index", () => {
115+
it("registers the builtin ollama embedding provider", () => {
116+
const adapter = listRegisteredAdapters().find((entry) => entry.id === "ollama");
117+
118+
expect(adapter).toBeDefined();
119+
expect(adapter).toEqual(
120+
expect.objectContaining({
121+
id: "ollama",
122+
defaultModel: DEFAULT_OLLAMA_EMBEDDING_MODEL,
123+
}),
124+
);
125+
});
126+
111127
let fixtureRoot = "";
112128
let workspaceDir = "";
113129
let memoryDir = "";

0 commit comments

Comments
 (0)