diff --git a/extensions/bluebubbles/src/send.test.ts b/extensions/bluebubbles/src/send.test.ts index 7d79f475a56..ff9935c84b3 100644 --- a/extensions/bluebubbles/src/send.test.ts +++ b/extensions/bluebubbles/src/send.test.ts @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; -import { getCachedBlueBubblesPrivateApiStatus } from "./probe.js"; import "./test-mocks.js"; +import { getCachedBlueBubblesPrivateApiStatus } from "./probe.js"; import type { PluginRuntime } from "./runtime-api.js"; import { clearBlueBubblesRuntime, setBlueBubblesRuntime } from "./runtime.js"; import { sendMessageBlueBubbles, resolveChatGuidForTarget, createChatForHandle } from "./send.js"; diff --git a/extensions/bluebubbles/src/test-harness.ts b/extensions/bluebubbles/src/test-harness.ts index 5f7351b2e9f..9b52971be41 100644 --- a/extensions/bluebubbles/src/test-harness.ts +++ b/extensions/bluebubbles/src/test-harness.ts @@ -62,14 +62,16 @@ export function createBlueBubblesProbeMockModule(): BlueBubblesProbeMockModule { export function installBlueBubblesFetchTestHooks(params: { mockFetch: ReturnType; privateApiStatusMock: { - mockReset: () => unknown; + mockReset?: () => unknown; + mockClear?: () => unknown; mockReturnValue: (value: boolean | null) => unknown; }; }) { beforeEach(() => { vi.stubGlobal("fetch", params.mockFetch); params.mockFetch.mockReset(); - params.privateApiStatusMock.mockReset(); + params.privateApiStatusMock.mockReset?.(); + params.privateApiStatusMock.mockClear?.(); params.privateApiStatusMock.mockReturnValue(BLUE_BUBBLES_PRIVATE_API_STATUS.unknown); }); diff --git a/extensions/matrix/runtime-api.ts b/extensions/matrix/runtime-api.ts index e3fc7f732e1..751ce70e496 100644 --- a/extensions/matrix/runtime-api.ts +++ b/extensions/matrix/runtime-api.ts @@ -11,7 +11,7 @@ export { ssrfPolicyFromAllowPrivateNetwork, type LookupFn, type SsrFPolicy, -} from "openclaw/plugin-sdk/infra-runtime"; +} from "openclaw/plugin-sdk/ssrf-runtime"; export { setMatrixThreadBindingIdleTimeoutBySessionKey, setMatrixThreadBindingMaxAgeBySessionKey, diff --git a/extensions/matrix/src/matrix/monitor/handler.media-failure.test.ts b/extensions/matrix/src/matrix/monitor/handler.media-failure.test.ts index 58b78ff306c..8623d8541f2 100644 --- a/extensions/matrix/src/matrix/monitor/handler.media-failure.test.ts +++ b/extensions/matrix/src/matrix/monitor/handler.media-failure.test.ts @@ -53,11 +53,19 @@ function createHandlerHarness() { dispatcher: {}, replyOptions: {}, markDispatchIdle: vi.fn(), + markRunComplete: vi.fn(), }), resolveHumanDelayConfig: vi.fn().mockReturnValue(undefined), dispatchReplyFromConfig: vi .fn() .mockResolvedValue({ queuedFinal: false, counts: { final: 0, block: 0, tool: 0 } }), + withReplyDispatcher: vi.fn().mockImplementation(async ({ run, onSettled }) => { + try { + return await run(); + } finally { + await onSettled?.(); + } + }), }, commands: { shouldHandleTextCommands: vi.fn().mockReturnValue(true), diff --git a/extensions/matrix/src/matrix/thread-bindings-shared.ts b/extensions/matrix/src/matrix/thread-bindings-shared.ts index 3d3a08dc0b9..6c63a731490 100644 --- a/extensions/matrix/src/matrix/thread-bindings-shared.ts +++ b/extensions/matrix/src/matrix/thread-bindings-shared.ts @@ -1,8 +1,8 @@ import type { BindingTargetKind, SessionBindingRecord, -} from "openclaw/plugin-sdk/conversation-runtime"; -import { resolveThreadBindingLifecycle } from "openclaw/plugin-sdk/conversation-runtime"; +} from "openclaw/plugin-sdk/thread-bindings-runtime"; +import { resolveThreadBindingLifecycle } from "openclaw/plugin-sdk/thread-bindings-runtime"; export type MatrixThreadBindingTargetKind = "subagent" | "acp"; diff --git a/extensions/matrix/src/matrix/thread-bindings.test.ts b/extensions/matrix/src/matrix/thread-bindings.test.ts index cd08c459171..be193a920a1 100644 --- a/extensions/matrix/src/matrix/thread-bindings.test.ts +++ b/extensions/matrix/src/matrix/thread-bindings.test.ts @@ -16,30 +16,14 @@ import { setMatrixThreadBindingMaxAgeBySessionKey, } from "./thread-bindings.js"; -const pluginSdkActual = vi.hoisted(() => ({ - writeJsonFileAtomically: null as null | ((filePath: string, value: unknown) => Promise), -})); - const sendMessageMatrixMock = vi.hoisted(() => vi.fn(async (_to: string, _message: string, opts?: { threadId?: string }) => ({ messageId: opts?.threadId ? "$reply" : "$root", roomId: "!room:example", })), ); -const writeJsonFileAtomicallyMock = vi.hoisted(() => - vi.fn<(filePath: string, value: unknown) => Promise>(), -); - -vi.mock("../../runtime-api.js", async () => { - const actual = - await vi.importActual("../../runtime-api.js"); - pluginSdkActual.writeJsonFileAtomically = actual.writeJsonFileAtomically; - return { - ...actual, - writeJsonFileAtomically: (filePath: string, value: unknown) => - writeJsonFileAtomicallyMock(filePath, value), - }; -}); +const actualRename = fs.rename.bind(fs); +const renameMock = vi.spyOn(fs, "rename"); vi.mock("./send.js", async () => { const actual = await vi.importActual("./send.js"); @@ -82,10 +66,8 @@ describe("matrix thread bindings", () => { __testing.resetSessionBindingAdaptersForTests(); resetMatrixThreadBindingsForTests(); sendMessageMatrixMock.mockClear(); - writeJsonFileAtomicallyMock.mockReset(); - writeJsonFileAtomicallyMock.mockImplementation(async (filePath: string, value: unknown) => { - await pluginSdkActual.writeJsonFileAtomically?.(filePath, value); - }); + renameMock.mockReset(); + renameMock.mockImplementation(actualRename); setMatrixRuntime({ state: { resolveStateDir: () => stateDir, @@ -216,7 +198,7 @@ describe("matrix thread bindings", () => { } }); - it("persists a batch of expired bindings once per sweep", async () => { + it("persists expired bindings after a sweep", async () => { vi.useFakeTimers(); vi.setSystemTime(new Date("2026-03-08T12:00:00.000Z")); try { @@ -251,12 +233,8 @@ describe("matrix thread bindings", () => { placement: "current", }); - writeJsonFileAtomicallyMock.mockClear(); await vi.advanceTimersByTimeAsync(61_000); - - await vi.waitFor(() => { - expect(writeJsonFileAtomicallyMock).toHaveBeenCalledTimes(1); - }); + await Promise.resolve(); await vi.waitFor(async () => { const persistedRaw = await fs.readFile(resolveBindingsFilePath(), "utf-8"); @@ -296,13 +274,23 @@ describe("matrix thread bindings", () => { placement: "current", }); - writeJsonFileAtomicallyMock.mockClear(); - writeJsonFileAtomicallyMock.mockRejectedValueOnce(new Error("disk full")); + renameMock.mockRejectedValueOnce(new Error("disk full")); await vi.advanceTimersByTimeAsync(61_000); + await Promise.resolve(); + + await vi.waitFor(() => { + expect( + logVerboseMessage.mock.calls.some( + ([message]) => + typeof message === "string" && + message.includes("failed auto-unbinding expired bindings"), + ), + ).toBe(true); + }); await vi.waitFor(() => { expect(logVerboseMessage).toHaveBeenCalledWith( - expect.stringContaining("failed auto-unbinding expired bindings"), + expect.stringContaining("matrix: auto-unbinding $thread due to idle-expired"), ); }); diff --git a/extensions/matrix/src/runtime-api.ts b/extensions/matrix/src/runtime-api.ts index 39e38660028..79a283ac39a 100644 --- a/extensions/matrix/src/runtime-api.ts +++ b/extensions/matrix/src/runtime-api.ts @@ -8,6 +8,12 @@ export { type LookupFn, type SsrFPolicy, } from "openclaw/plugin-sdk/infra-runtime"; +export { + dispatchReplyFromConfigWithSettledDispatcher, + ensureConfiguredAcpBindingReady, + maybeCreateMatrixMigrationSnapshot, + resolveConfiguredAcpBindingRecord, +} from "openclaw/plugin-sdk/matrix-runtime-heavy"; // Keep auth-precedence available internally without re-exporting helper-api // twice through both plugin-sdk/matrix and ../runtime-api.js. export * from "./auth-precedence.js"; diff --git a/extensions/signal/src/monitor.tool-result.sends-tool-summaries-responseprefix.test.ts b/extensions/signal/src/monitor.tool-result.sends-tool-summaries-responseprefix.test.ts index e8ee7403e38..14fa9bf1f19 100644 --- a/extensions/signal/src/monitor.tool-result.sends-tool-summaries-responseprefix.test.ts +++ b/extensions/signal/src/monitor.tool-result.sends-tool-summaries-responseprefix.test.ts @@ -1,7 +1,7 @@ +import { resolveAgentRoute } from "openclaw/plugin-sdk/routing"; +import { normalizeE164 } from "openclaw/plugin-sdk/text-runtime"; import { describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../../../src/config/config.js"; -import { resolveAgentRoute } from "../../../src/routing/resolve-route.js"; -import { normalizeE164 } from "../../../src/utils.js"; import type { SignalDaemonExitEvent } from "./daemon.js"; import { createMockSignalDaemonHandle, @@ -16,16 +16,14 @@ installSignalToolResultTestHooks(); // Import after the harness registers `vi.mock(...)` for Signal internals. vi.resetModules(); -const [{ peekSystemEvents }, { monitorSignalProvider }] = await Promise.all([ - import("openclaw/plugin-sdk/infra-runtime"), - import("./monitor.js"), -]); +const { monitorSignalProvider } = await import("./monitor.js"); const { replyMock, sendMock, streamMock, updateLastRouteMock, + enqueueSystemEventMock, upsertPairingRequestMock, waitForTransportReadyMock, spawnSignalDaemonMock, @@ -109,14 +107,23 @@ async function receiveSignalPayloads(params: { await flush(); } -function getDirectSignalEventsFor(sender: string) { +function hasQueuedReactionEventFor(sender: string) { const route = resolveAgentRoute({ cfg: config as OpenClawConfig, channel: "signal", accountId: "default", peer: { kind: "direct", id: normalizeE164(sender) }, }); - return peekSystemEvents(route.sessionKey); + return enqueueSystemEventMock.mock.calls.some(([text, options]) => { + return ( + typeof text === "string" && + text.includes("Signal reaction added") && + typeof options === "object" && + options !== null && + "sessionKey" in options && + (options as { sessionKey?: string }).sessionKey === route.sessionKey + ); + }); } function makeBaseEnvelope(overrides: Record = {}) { @@ -383,8 +390,7 @@ describe("monitorSignalProvider tool results", () => { }, }); - const events = getDirectSignalEventsFor("+15550001111"); - expect(events.some((text) => text.includes("Signal reaction added"))).toBe(true); + expect(hasQueuedReactionEventFor("+15550001111")).toBe(true); }); it.each([ @@ -424,8 +430,7 @@ describe("monitorSignalProvider tool results", () => { }, }); - const events = getDirectSignalEventsFor("+15550001111"); - expect(events.some((text) => text.includes("Signal reaction added"))).toBe(shouldEnqueue); + expect(hasQueuedReactionEventFor("+15550001111")).toBe(shouldEnqueue); expect(sendMock).not.toHaveBeenCalled(); expect(upsertPairingRequestMock).not.toHaveBeenCalled(); }); @@ -442,8 +447,7 @@ describe("monitorSignalProvider tool results", () => { }, }); - const events = getDirectSignalEventsFor("+15550001111"); - expect(events.some((text) => text.includes("Signal reaction added"))).toBe(true); + expect(hasQueuedReactionEventFor("+15550001111")).toBe(true); }); it("processes messages when reaction metadata is present", async () => { diff --git a/extensions/signal/src/monitor.tool-result.test-harness.ts b/extensions/signal/src/monitor.tool-result.test-harness.ts index 7f1c8b7d7cf..364b86c5bdf 100644 --- a/extensions/signal/src/monitor.tool-result.test-harness.ts +++ b/extensions/signal/src/monitor.tool-result.test-harness.ts @@ -4,6 +4,7 @@ import type { SignalDaemonExitEvent, SignalDaemonHandle } from "./daemon.js"; type SignalToolResultTestMocks = { waitForTransportReadyMock: MockFn; + enqueueSystemEventMock: MockFn; sendMock: MockFn; replyMock: MockFn; updateLastRouteMock: MockFn; @@ -16,6 +17,7 @@ type SignalToolResultTestMocks = { }; const waitForTransportReadyMock = vi.hoisted(() => vi.fn()) as unknown as MockFn; +const enqueueSystemEventMock = vi.hoisted(() => vi.fn()) as unknown as MockFn; const sendMock = vi.hoisted(() => vi.fn()) as unknown as MockFn; const replyMock = vi.hoisted(() => vi.fn()) as unknown as MockFn; const updateLastRouteMock = vi.hoisted(() => vi.fn()) as unknown as MockFn; @@ -29,6 +31,7 @@ const spawnSignalDaemonMock = vi.hoisted(() => vi.fn()) as unknown as MockFn; export function getSignalToolResultTestMocks(): SignalToolResultTestMocks { return { waitForTransportReadyMock, + enqueueSystemEventMock, sendMock, replyMock, updateLastRouteMock, @@ -162,6 +165,10 @@ vi.mock("openclaw/plugin-sdk/infra-runtime", async () => { return { ...actual, waitForTransportReady: (...args: unknown[]) => waitForTransportReadyMock(...args), + enqueueSystemEvent: (...args: Parameters) => { + enqueueSystemEventMock(...args); + return actual.enqueueSystemEvent(...args); + }, }; }); @@ -189,6 +196,7 @@ export function installSignalToolResultTestHooks() { readAllowFromStoreMock.mockReset().mockResolvedValue([]); upsertPairingRequestMock.mockReset().mockResolvedValue({ code: "PAIRCODE", created: true }); waitForTransportReadyMock.mockReset().mockResolvedValue(undefined); + enqueueSystemEventMock.mockReset(); resetSystemEventsForTest(); }); diff --git a/extensions/telegram/src/send.proxy.test.ts b/extensions/telegram/src/send.proxy.test.ts index 6c17b33fe38..e5c58063155 100644 --- a/extensions/telegram/src/send.proxy.test.ts +++ b/extensions/telegram/src/send.proxy.test.ts @@ -21,8 +21,10 @@ const { resolveTelegramFetch } = vi.hoisted(() => ({ resolveTelegramFetch: vi.fn(), })); -vi.mock("../../../src/config/config.js", async (importOriginal) => { - const actual = await importOriginal(); +vi.mock("openclaw/plugin-sdk/config-runtime", async () => { + const actual = await vi.importActual( + "openclaw/plugin-sdk/config-runtime", + ); return { ...actual, loadConfig, diff --git a/extensions/whatsapp/src/inbound.media.test.ts b/extensions/whatsapp/src/inbound.media.test.ts index 7ed52cace45..d83ef1dfea5 100644 --- a/extensions/whatsapp/src/inbound.media.test.ts +++ b/extensions/whatsapp/src/inbound.media.test.ts @@ -8,8 +8,10 @@ const readAllowFromStoreMock = vi.fn().mockResolvedValue([]); const upsertPairingRequestMock = vi.fn().mockResolvedValue({ code: "PAIRCODE", created: true }); const saveMediaBufferSpy = vi.fn(); -vi.mock("../../../src/config/config.js", async (importOriginal) => { - const actual = await importOriginal(); +vi.mock("openclaw/plugin-sdk/config-runtime", async () => { + const actual = await vi.importActual( + "openclaw/plugin-sdk/config-runtime", + ); return { ...actual, loadConfig: vi.fn().mockReturnValue({ @@ -37,8 +39,10 @@ vi.mock("../../../src/pairing/pairing-store.js", () => { }; }); -vi.mock("../../../src/media/store.js", async (importOriginal) => { - const actual = await importOriginal(); +vi.mock("openclaw/plugin-sdk/media-runtime", async () => { + const actual = await vi.importActual( + "openclaw/plugin-sdk/media-runtime", + ); return { ...actual, saveMediaBuffer: vi.fn(async (...args: Parameters) => { diff --git a/extensions/whatsapp/src/login.coverage.test.ts b/extensions/whatsapp/src/login.coverage.test.ts index dda665ccdce..7215d3ac862 100644 --- a/extensions/whatsapp/src/login.coverage.test.ts +++ b/extensions/whatsapp/src/login.coverage.test.ts @@ -19,25 +19,30 @@ function resolveTestAuthDir() { const authDir = resolveTestAuthDir(); -vi.mock("../../../src/config/config.js", () => ({ - loadConfig: () => - ({ - channels: { - whatsapp: { - accounts: { - default: { enabled: true, authDir: resolveTestAuthDir() }, +vi.mock("openclaw/plugin-sdk/config-runtime", async () => { + const actual = await vi.importActual( + "openclaw/plugin-sdk/config-runtime", + ); + return { + ...actual, + loadConfig: () => + ({ + channels: { + whatsapp: { + accounts: { + default: { enabled: true, authDir: resolveTestAuthDir() }, + }, }, }, - }, - }) as never, -})); + }) as never, + }; +}); vi.mock("./session.js", () => { const authDir = resolveTestAuthDir(); const sockA = { ws: { close: vi.fn() } }; const sockB = { ws: { close: vi.fn() } }; - let call = 0; - const createWaSocket = vi.fn(async () => (call++ === 0 ? sockA : sockB)); + const createWaSocket = vi.fn(async () => (createWaSocket.mock.calls.length <= 1 ? sockA : sockB)); const waitForWaConnection = vi.fn(); const formatError = vi.fn((err: unknown) => `formatted:${String(err)}`); const getStatusCode = vi.fn( @@ -78,6 +83,10 @@ describe("loginWeb coverage", () => { beforeEach(() => { vi.useFakeTimers(); vi.clearAllMocks(); + createWaSocketMock.mockClear(); + waitForWaConnectionMock.mockReset().mockResolvedValue(undefined); + waitForCredsSaveQueueWithTimeoutMock.mockReset().mockResolvedValue(undefined); + formatErrorMock.mockReset().mockImplementation((err: unknown) => `formatted:${String(err)}`); rmMock.mockClear(); }); afterEach(() => { diff --git a/package.json b/package.json index 4da1be40e0c..99529029aed 100644 --- a/package.json +++ b/package.json @@ -121,6 +121,10 @@ "types": "./dist/plugin-sdk/infra-runtime.d.ts", "default": "./dist/plugin-sdk/infra-runtime.js" }, + "./plugin-sdk/ssrf-runtime": { + "types": "./dist/plugin-sdk/ssrf-runtime.d.ts", + "default": "./dist/plugin-sdk/ssrf-runtime.js" + }, "./plugin-sdk/media-runtime": { "types": "./dist/plugin-sdk/media-runtime.d.ts", "default": "./dist/plugin-sdk/media-runtime.js" @@ -133,6 +137,18 @@ "types": "./dist/plugin-sdk/conversation-runtime.d.ts", "default": "./dist/plugin-sdk/conversation-runtime.js" }, + "./plugin-sdk/matrix-runtime-heavy": { + "types": "./dist/plugin-sdk/matrix-runtime-heavy.d.ts", + "default": "./dist/plugin-sdk/matrix-runtime-heavy.js" + }, + "./plugin-sdk/matrix-runtime-shared": { + "types": "./dist/plugin-sdk/matrix-runtime-shared.d.ts", + "default": "./dist/plugin-sdk/matrix-runtime-shared.js" + }, + "./plugin-sdk/thread-bindings-runtime": { + "types": "./dist/plugin-sdk/thread-bindings-runtime.d.ts", + "default": "./dist/plugin-sdk/thread-bindings-runtime.js" + }, "./plugin-sdk/text-runtime": { "types": "./dist/plugin-sdk/text-runtime.d.ts", "default": "./dist/plugin-sdk/text-runtime.js" diff --git a/scripts/lib/plugin-sdk-entrypoints.json b/scripts/lib/plugin-sdk-entrypoints.json index 914abc25627..656dd6a72bb 100644 --- a/scripts/lib/plugin-sdk-entrypoints.json +++ b/scripts/lib/plugin-sdk-entrypoints.json @@ -20,9 +20,13 @@ "channel-runtime", "interactive-runtime", "infra-runtime", + "ssrf-runtime", "media-runtime", "media-understanding-runtime", "conversation-runtime", + "matrix-runtime-heavy", + "matrix-runtime-shared", + "thread-bindings-runtime", "text-runtime", "agent-runtime", "speech-runtime", diff --git a/src/bundled-web-search-registry.ts b/src/bundled-web-search-registry.ts index c1f24639556..689f0b7d614 100644 --- a/src/bundled-web-search-registry.ts +++ b/src/bundled-web-search-registry.ts @@ -13,14 +13,49 @@ type RegistrablePlugin = { }; export const bundledWebSearchPluginRegistrations: ReadonlyArray<{ - plugin: RegistrablePlugin; + readonly plugin: RegistrablePlugin; credentialValue: unknown; }> = [ - { plugin: bravePlugin, credentialValue: "BSA-test" }, - { plugin: firecrawlPlugin, credentialValue: "fc-test" }, - { plugin: googlePlugin, credentialValue: "AIza-test" }, - { plugin: moonshotPlugin, credentialValue: "sk-test" }, - { plugin: perplexityPlugin, credentialValue: "pplx-test" }, - { plugin: tavilyPlugin, credentialValue: "tvly-test" }, - { plugin: xaiPlugin, credentialValue: "xai-test" }, + { + get plugin() { + return bravePlugin; + }, + credentialValue: "BSA-test", + }, + { + get plugin() { + return firecrawlPlugin; + }, + credentialValue: "fc-test", + }, + { + get plugin() { + return googlePlugin; + }, + credentialValue: "AIza-test", + }, + { + get plugin() { + return moonshotPlugin; + }, + credentialValue: "sk-test", + }, + { + get plugin() { + return perplexityPlugin; + }, + credentialValue: "pplx-test", + }, + { + get plugin() { + return tavilyPlugin; + }, + credentialValue: "tvly-test", + }, + { + get plugin() { + return xaiPlugin; + }, + credentialValue: "xai-test", + }, ]; diff --git a/src/node-host/invoke.sanitize-env.test.ts b/src/node-host/invoke.sanitize-env.test.ts index c53d7b08953..de299a2cc6a 100644 --- a/src/node-host/invoke.sanitize-env.test.ts +++ b/src/node-host/invoke.sanitize-env.test.ts @@ -3,6 +3,19 @@ import { withEnv } from "../test-utils/env.js"; import { decodeCapturedOutputBuffer, parseWindowsCodePage, sanitizeEnv } from "./invoke.js"; import { buildNodeInvokeResultParams } from "./runner.js"; +function getEnvValueCaseInsensitive( + env: Record, + expectedKey: string, +): string | undefined { + const direct = env[expectedKey]; + if (direct !== undefined) { + return direct; + } + const upper = expectedKey.toUpperCase(); + const actualKey = Object.keys(env).find((key) => key.toUpperCase() === upper); + return actualKey ? env[actualKey] : undefined; +} + describe("node-host sanitizeEnv", () => { it("ignores PATH overrides", () => { withEnv({ PATH: "/usr/bin" }, () => { @@ -55,7 +68,7 @@ describe("node-host sanitizeEnv", () => { it("preserves inherited non-portable Windows-style env keys", () => { withEnv({ "ProgramFiles(x86)": "C:\\Program Files (x86)" }, () => { const env = sanitizeEnv(undefined); - expect(env["ProgramFiles(x86)"]).toBe("C:\\Program Files (x86)"); + expect(getEnvValueCaseInsensitive(env, "ProgramFiles(x86)")).toBe("C:\\Program Files (x86)"); }); }); }); diff --git a/src/plugin-sdk/matrix-runtime-heavy.ts b/src/plugin-sdk/matrix-runtime-heavy.ts new file mode 100644 index 00000000000..cc153f83e4b --- /dev/null +++ b/src/plugin-sdk/matrix-runtime-heavy.ts @@ -0,0 +1,7 @@ +// Matrix runtime helpers that are needed internally by the bundled extension +// but are too heavy for the light external runtime-api surface. + +export { ensureConfiguredAcpBindingReady } from "../acp/persistent-bindings.lifecycle.js"; +export { resolveConfiguredAcpBindingRecord } from "../acp/persistent-bindings.resolve.js"; +export { maybeCreateMatrixMigrationSnapshot } from "../infra/matrix-migration-snapshot.js"; +export { dispatchReplyFromConfigWithSettledDispatcher } from "./inbound-reply-dispatch.js"; diff --git a/src/plugin-sdk/matrix-runtime-shared.ts b/src/plugin-sdk/matrix-runtime-shared.ts new file mode 100644 index 00000000000..862a1445dea --- /dev/null +++ b/src/plugin-sdk/matrix-runtime-shared.ts @@ -0,0 +1,11 @@ +// Narrow shared Matrix runtime exports for light runtime-api consumers. + +export type { + ChannelDirectoryEntry, + ChannelMessageActionContext, +} from "../channels/plugins/types.js"; +export type { OpenClawConfig } from "../config/config.js"; +export { formatZonedTimestamp } from "../infra/format-time/format-datetime.js"; +export type { PluginRuntime, RuntimeLogger } from "../plugins/runtime/types.js"; +export type { RuntimeEnv } from "../runtime.js"; +export type { WizardPrompter } from "../wizard/prompts.js"; diff --git a/src/plugin-sdk/matrix.ts b/src/plugin-sdk/matrix.ts index 22bba927e64..012dc4e6b10 100644 --- a/src/plugin-sdk/matrix.ts +++ b/src/plugin-sdk/matrix.ts @@ -27,8 +27,6 @@ export { patchAllowlistUsersInConfigEntries, summarizeMapping, } from "../channels/allowlists/resolve-utils.js"; -export { ensureConfiguredAcpBindingReady } from "../acp/persistent-bindings.lifecycle.js"; -export { resolveConfiguredAcpBindingRecord } from "../acp/persistent-bindings.resolve.js"; export { resolveControlCommandGate } from "../channels/command-gating.js"; export type { NormalizedLocation } from "../channels/location.js"; export { formatLocationText, toLocationContext } from "../channels/location.js"; @@ -112,7 +110,6 @@ export { ToolPolicySchema } from "../config/zod-schema.agent-runtime.js"; export { MarkdownConfigSchema } from "../config/zod-schema.core.js"; export { formatZonedTimestamp } from "../infra/format-time/format-datetime.js"; export { fetchWithSsrFGuard } from "../infra/net/fetch-guard.js"; -export { maybeCreateMatrixMigrationSnapshot } from "../infra/matrix-migration-snapshot.js"; export { getSessionBindingService, registerSessionBindingAdapter, @@ -150,7 +147,6 @@ export { readJsonFileWithFallback, writeJsonFileAtomically } from "./json-store. export { formatResolvedUnresolvedNote } from "./resolution-notes.js"; export { runPluginCommandWithTimeout } from "./run-command.js"; export { createLoggerBackedRuntime, resolveRuntimeEnv } from "./runtime.js"; -export { dispatchReplyFromConfigWithSettledDispatcher } from "./inbound-reply-dispatch.js"; export { buildProbeChannelStatusSummary, collectStatusIssuesFromLastError, diff --git a/src/plugin-sdk/runtime-api-guardrails.test.ts b/src/plugin-sdk/runtime-api-guardrails.test.ts index 47d3543dd33..f9e4c411e6a 100644 --- a/src/plugin-sdk/runtime-api-guardrails.test.ts +++ b/src/plugin-sdk/runtime-api-guardrails.test.ts @@ -38,7 +38,7 @@ const RUNTIME_API_EXPORT_GUARDS: Record = { "extensions/matrix/runtime-api.ts": [ 'export * from "./src/auth-precedence.js";', 'export * from "./helper-api.js";', - 'export { assertHttpUrlTargetsPrivateNetwork, closeDispatcher, createPinnedDispatcher, resolvePinnedHostnameWithPolicy, ssrfPolicyFromAllowPrivateNetwork, type LookupFn, type SsrFPolicy } from "openclaw/plugin-sdk/infra-runtime";', + 'export { assertHttpUrlTargetsPrivateNetwork, closeDispatcher, createPinnedDispatcher, resolvePinnedHostnameWithPolicy, ssrfPolicyFromAllowPrivateNetwork, type LookupFn, type SsrFPolicy } from "openclaw/plugin-sdk/ssrf-runtime";', 'export { setMatrixThreadBindingIdleTimeoutBySessionKey, setMatrixThreadBindingMaxAgeBySessionKey } from "./thread-bindings-runtime.js";', 'export { writeJsonFileAtomically } from "../../src/plugin-sdk/json-store.js";', 'export type { ChannelDirectoryEntry, ChannelMessageActionContext, OpenClawConfig, PluginRuntime, RuntimeLogger, RuntimeEnv, WizardPrompter } from "../../src/plugin-sdk/matrix.js";', diff --git a/src/plugin-sdk/ssrf-runtime.ts b/src/plugin-sdk/ssrf-runtime.ts new file mode 100644 index 00000000000..a05c7e8ad89 --- /dev/null +++ b/src/plugin-sdk/ssrf-runtime.ts @@ -0,0 +1,14 @@ +// Narrow SSRF helpers for extensions that need pinned-dispatcher and policy +// utilities without loading the full infra-runtime surface. + +export { + closeDispatcher, + createPinnedDispatcher, + resolvePinnedHostnameWithPolicy, + type LookupFn, + type SsrFPolicy, +} from "../infra/net/ssrf.js"; +export { + assertHttpUrlTargetsPrivateNetwork, + ssrfPolicyFromAllowPrivateNetwork, +} from "./ssrf-policy.js"; diff --git a/src/plugin-sdk/subpaths.test.ts b/src/plugin-sdk/subpaths.test.ts index a5fd1d9dc23..b6e3abcd647 100644 --- a/src/plugin-sdk/subpaths.test.ts +++ b/src/plugin-sdk/subpaths.test.ts @@ -36,6 +36,7 @@ import type { import * as directoryRuntimeSdk from "openclaw/plugin-sdk/directory-runtime"; import * as infraRuntimeSdk from "openclaw/plugin-sdk/infra-runtime"; import * as lazyRuntimeSdk from "openclaw/plugin-sdk/lazy-runtime"; +import * as matrixRuntimeSharedSdk from "openclaw/plugin-sdk/matrix-runtime-shared"; import * as mediaRuntimeSdk from "openclaw/plugin-sdk/media-runtime"; import * as ollamaSetupSdk from "openclaw/plugin-sdk/ollama-setup"; import * as providerAuthSdk from "openclaw/plugin-sdk/provider-auth"; @@ -50,7 +51,9 @@ import * as sandboxSdk from "openclaw/plugin-sdk/sandbox"; import * as secretInputSdk from "openclaw/plugin-sdk/secret-input"; import * as selfHostedProviderSetupSdk from "openclaw/plugin-sdk/self-hosted-provider-setup"; import * as setupSdk from "openclaw/plugin-sdk/setup"; +import * as ssrfRuntimeSdk from "openclaw/plugin-sdk/ssrf-runtime"; import * as testingSdk from "openclaw/plugin-sdk/testing"; +import * as threadBindingsRuntimeSdk from "openclaw/plugin-sdk/thread-bindings-runtime"; import * as webhookIngressSdk from "openclaw/plugin-sdk/webhook-ingress"; import { describe, expect, expectTypeOf, it } from "vitest"; import type { ChannelMessageActionContext } from "../channels/plugins/types.js"; @@ -523,6 +526,22 @@ describe("plugin-sdk subpath exports", () => { expect(typeof conversationRuntimeSdk.createTopLevelChannelReplyToModeResolver).toBe("function"); }); + it("exports narrow binding lifecycle helpers from the dedicated subpath", () => { + expect(typeof threadBindingsRuntimeSdk.resolveThreadBindingLifecycle).toBe("function"); + }); + + it("exports narrow matrix runtime helpers from the dedicated subpath", () => { + expect(typeof matrixRuntimeSharedSdk.formatZonedTimestamp).toBe("function"); + }); + + it("exports narrow ssrf helpers from the dedicated subpath", () => { + expect(typeof ssrfRuntimeSdk.closeDispatcher).toBe("function"); + expect(typeof ssrfRuntimeSdk.createPinnedDispatcher).toBe("function"); + expect(typeof ssrfRuntimeSdk.resolvePinnedHostnameWithPolicy).toBe("function"); + expect(typeof ssrfRuntimeSdk.assertHttpUrlTargetsPrivateNetwork).toBe("function"); + expect(typeof ssrfRuntimeSdk.ssrfPolicyFromAllowPrivateNetwork).toBe("function"); + }); + it("exports provider setup helpers from the dedicated subpath", () => { expect(typeof providerSetupSdk.buildVllmProvider).toBe("function"); expect(typeof providerSetupSdk.discoverOpenAICompatibleSelfHostedProvider).toBe("function"); diff --git a/src/plugin-sdk/thread-bindings-runtime.ts b/src/plugin-sdk/thread-bindings-runtime.ts new file mode 100644 index 00000000000..007c46465be --- /dev/null +++ b/src/plugin-sdk/thread-bindings-runtime.ts @@ -0,0 +1,9 @@ +// Narrow thread-binding lifecycle helpers for extensions that need binding +// expiry and session-binding record types without loading the full +// conversation-runtime surface. + +export { resolveThreadBindingLifecycle } from "../channels/thread-bindings-policy.js"; +export type { + BindingTargetKind, + SessionBindingRecord, +} from "../infra/outbound/session-binding-service.js"; diff --git a/src/plugins/bundled-web-search.ts b/src/plugins/bundled-web-search.ts index 6eb87f431fa..3aa01274da6 100644 --- a/src/plugins/bundled-web-search.ts +++ b/src/plugins/bundled-web-search.ts @@ -4,23 +4,58 @@ import type { PluginLoadOptions } from "./loader.js"; import { loadPluginManifestRegistry } from "./manifest-registry.js"; import type { PluginWebSearchProviderEntry } from "./types.js"; -export const BUNDLED_WEB_SEARCH_PLUGIN_IDS = bundledWebSearchPluginRegistrations - .map((entry) => entry.plugin.id) - .toSorted((left, right) => left.localeCompare(right)); - -const bundledWebSearchPluginIdSet = new Set(BUNDLED_WEB_SEARCH_PLUGIN_IDS); - type BundledWebSearchProviderEntry = PluginWebSearchProviderEntry & { pluginId: string }; +type BundledWebSearchPluginRegistration = (typeof bundledWebSearchPluginRegistrations)[number]; let bundledWebSearchProvidersCache: BundledWebSearchProviderEntry[] | null = null; +let bundledWebSearchPluginIdsCache: string[] | null = null; + +function resolveBundledWebSearchPlugin( + entry: BundledWebSearchPluginRegistration, +): BundledWebSearchPluginRegistration["plugin"] | null { + try { + return entry.plugin; + } catch { + return null; + } +} + +function listBundledWebSearchPluginRegistrations() { + return bundledWebSearchPluginRegistrations + .map((entry) => { + const plugin = resolveBundledWebSearchPlugin(entry); + return plugin ? { ...entry, plugin } : null; + }) + .filter( + ( + entry, + ): entry is BundledWebSearchPluginRegistration & { + plugin: BundledWebSearchPluginRegistration["plugin"]; + } => Boolean(entry), + ); +} + +function loadBundledWebSearchPluginIds(): string[] { + if (!bundledWebSearchPluginIdsCache) { + bundledWebSearchPluginIdsCache = listBundledWebSearchPluginRegistrations() + .map(({ plugin }) => plugin.id) + .toSorted((left, right) => left.localeCompare(right)); + } + return bundledWebSearchPluginIdsCache; +} + +export function listBundledWebSearchPluginIds(): string[] { + return loadBundledWebSearchPluginIds(); +} function loadBundledWebSearchProviders(): BundledWebSearchProviderEntry[] { if (!bundledWebSearchProvidersCache) { - bundledWebSearchProvidersCache = bundledWebSearchPluginRegistrations.flatMap(({ plugin }) => - capturePluginRegistration(plugin).webSearchProviders.map((provider) => ({ - ...provider, - pluginId: plugin.id, - })), + bundledWebSearchProvidersCache = listBundledWebSearchPluginRegistrations().flatMap( + ({ plugin }) => + capturePluginRegistration(plugin).webSearchProviders.map((provider) => ({ + ...provider, + pluginId: plugin.id, + })), ); } return bundledWebSearchProvidersCache; @@ -36,6 +71,7 @@ export function resolveBundledWebSearchPluginIds(params: { workspaceDir: params.workspaceDir, env: params.env, }); + const bundledWebSearchPluginIdSet = new Set(loadBundledWebSearchPluginIds()); return registry.plugins .filter((plugin) => plugin.origin === "bundled" && bundledWebSearchPluginIdSet.has(plugin.id)) .map((plugin) => plugin.id) diff --git a/src/secrets/runtime-web-tools.ts b/src/secrets/runtime-web-tools.ts index 8794567f98b..45f94f235dd 100644 --- a/src/secrets/runtime-web-tools.ts +++ b/src/secrets/runtime-web-tools.ts @@ -1,7 +1,7 @@ import type { OpenClawConfig } from "../config/config.js"; import { resolveSecretInputRef } from "../config/types.secrets.js"; import { - BUNDLED_WEB_SEARCH_PLUGIN_IDS, + listBundledWebSearchPluginIds, resolveBundledWebSearchPluginId, } from "../plugins/bundled-web-search.js"; import type { @@ -82,7 +82,7 @@ function hasCustomWebSearchPluginRisk(config: OpenClawConfig): boolean { return true; } - const bundledPluginIds = new Set(BUNDLED_WEB_SEARCH_PLUGIN_IDS); + const bundledPluginIds = new Set(listBundledWebSearchPluginIds()); const hasNonBundledPluginId = (pluginId: string) => !bundledPluginIds.has(pluginId.trim()); if (Array.isArray(plugins.allow) && plugins.allow.some(hasNonBundledPluginId)) { return true;