diff --git a/CHANGELOG.md b/CHANGELOG.md index 171a8b00ef9..b85028835bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ Docs: https://docs.openclaw.ai - Agents/compaction: add `agents.defaults.compaction.notifyUser` so the `🧹 Compacting context...` start notice is opt-in instead of always being shown. (#54251) Thanks @oguricap0327. - Plugins/hooks: add `before_agent_reply` so plugins can short-circuit the LLM with synthetic replies after inline actions. (#20067) thanks @JoshuaLelon - Providers/runtime: add provider-owned replay hook surfaces for transcript policy, replay cleanup, and reasoning-mode dispatch. (#59143) Thanks @jalehman. + ### Fixes - Matrix/multi-account: keep room-level `account` scoping, inherited room overrides, and implicit account selection consistent across top-level default auth, named accounts, and cached-credential env setups. (#58449) thanks @Daanvdplas and @gumadeiras. @@ -24,6 +25,7 @@ Docs: https://docs.openclaw.ai - BlueBubbles/config: accept `enrichGroupParticipantsFromContacts` in the core strict config schema so gateways no longer fail validation or startup when the BlueBubbles plugin writes that field. (#56889) Thanks @zqchris. - Agents/failover: classify AbortError and stream-abort messages as timeout so Ollama NDJSON stream aborts stop showing `reason=unknown` in model fallback logs. (#58324) Thanks @yelog - Exec approvals: route Slack, Discord, and Telegram approvals through the shared channel approval-capability path so native approval auth, delivery, and `/approve` handling stay aligned across channels while preserving Telegram session-key agent filtering. (#58634) thanks @gumadeiras +- Matrix/runtime: resolve the verification/bootstrap runtime from a distinct packaged Matrix entry so global npm installs stop failing on crypto bootstrap with missing-module or recursive runtime alias errors. (#59249) Thanks @gumadeiras. ## 2026.4.2 diff --git a/extensions/matrix/plugin-entry.handlers.runtime.ts b/extensions/matrix/plugin-entry.handlers.runtime.ts new file mode 100644 index 00000000000..f9145dad943 --- /dev/null +++ b/extensions/matrix/plugin-entry.handlers.runtime.ts @@ -0,0 +1 @@ +export * from "./src/plugin-entry.runtime.ts"; diff --git a/extensions/matrix/src/plugin-entry.runtime.js b/extensions/matrix/src/plugin-entry.runtime.js index cd2cdd09a8e..35a83b504b8 100644 --- a/extensions/matrix/src/plugin-entry.runtime.js +++ b/extensions/matrix/src/plugin-entry.runtime.js @@ -1,6 +1,6 @@ // Thin ESM wrapper so native dynamic import() resolves in source-checkout mode -// where jiti loads index.ts but import("./src/plugin-entry.runtime.js") uses -// Node's native ESM loader which cannot resolve .ts files directly. +// while packaged dist builds resolve a distinct runtime entry that cannot loop +// back into this wrapper through the stable root runtime alias. import fs from "node:fs"; import { createRequire } from "node:module"; import path from "node:path"; @@ -9,9 +9,11 @@ import { fileURLToPath } from "node:url"; const require = createRequire(import.meta.url); const { createJiti } = require("jiti"); +const PLUGIN_ID = "matrix"; const OPENCLAW_PLUGIN_SDK_PREFIX = ["openclaw", "plugin-sdk"].join("/"); const PLUGIN_SDK_EXPORT_PREFIX = "./plugin-sdk/"; const PLUGIN_SDK_SOURCE_EXTENSIONS = [".ts", ".mts", ".js", ".mjs", ".cts", ".cjs"]; +const PLUGIN_ENTRY_RUNTIME_BASENAME = "plugin-entry.handlers.runtime"; const JITI_EXTENSIONS = [ ".ts", ".tsx", @@ -102,6 +104,42 @@ function buildPluginSdkAliasMap(moduleUrl) { return aliasMap; } +function resolveBundledPluginRuntimeModulePath(moduleUrl, params) { + const modulePath = fileURLToPath(moduleUrl); + const moduleDir = path.dirname(modulePath); + const localCandidates = [ + path.join(moduleDir, "..", params.runtimeBasename), + path.join(moduleDir, "extensions", params.pluginId, params.runtimeBasename), + ]; + + for (const candidate of localCandidates) { + const resolved = resolveExistingFile(candidate, PLUGIN_SDK_SOURCE_EXTENSIONS); + if (resolved) { + return resolved; + } + } + + const location = findOpenClawPackageRoot(moduleDir); + if (location) { + const { packageRoot } = location; + const packageCandidates = [ + path.join(packageRoot, "extensions", params.pluginId, params.runtimeBasename), + path.join(packageRoot, "dist", "extensions", params.pluginId, params.runtimeBasename), + ]; + + for (const candidate of packageCandidates) { + const resolved = resolveExistingFile(candidate, PLUGIN_SDK_SOURCE_EXTENSIONS); + if (resolved) { + return resolved; + } + } + } + + throw new Error( + `Cannot resolve ${params.pluginId} plugin runtime module ${params.runtimeBasename} from ${modulePath}`, + ); +} + const jiti = createJiti(import.meta.url, { alias: buildPluginSdkAliasMap(import.meta.url), interopDefault: true, @@ -109,7 +147,12 @@ const jiti = createJiti(import.meta.url, { extensions: JITI_EXTENSIONS, }); -const mod = jiti("./plugin-entry.runtime.ts"); +const mod = jiti( + resolveBundledPluginRuntimeModulePath(import.meta.url, { + pluginId: PLUGIN_ID, + runtimeBasename: PLUGIN_ENTRY_RUNTIME_BASENAME, + }), +); export const ensureMatrixCryptoRuntime = mod.ensureMatrixCryptoRuntime; export const handleVerifyRecoveryKey = mod.handleVerifyRecoveryKey; export const handleVerificationBootstrap = mod.handleVerificationBootstrap; diff --git a/extensions/matrix/src/plugin-entry.runtime.test.ts b/extensions/matrix/src/plugin-entry.runtime.test.ts index 537b8919c4e..6709894a84d 100644 --- a/extensions/matrix/src/plugin-entry.runtime.test.ts +++ b/extensions/matrix/src/plugin-entry.runtime.test.ts @@ -1,6 +1,39 @@ +import fs from "node:fs"; +import { createRequire } from "node:module"; +import os from "node:os"; import path from "node:path"; import { pathToFileURL } from "node:url"; -import { expect, it } from "vitest"; +import { afterEach, expect, it } from "vitest"; + +const tempDirs: string[] = []; +const REPO_ROOT = process.cwd(); +const require = createRequire(import.meta.url); +const JITI_ENTRY_PATH = require.resolve("jiti"); +const PACKAGED_RUNTIME_STUB = [ + "export async function ensureMatrixCryptoRuntime() {}", + "export async function handleVerifyRecoveryKey() {}", + "export async function handleVerificationBootstrap() {}", + "export async function handleVerificationStatus() {}", + "", +].join("\n"); + +function makeFixtureRoot(prefix: string) { + const fixtureRoot = fs.mkdtempSync(path.join(os.tmpdir(), prefix)); + tempDirs.push(fixtureRoot); + return fixtureRoot; +} + +function writeFixtureFile(fixtureRoot: string, relativePath: string, value: string) { + const fullPath = path.join(fixtureRoot, relativePath); + fs.mkdirSync(path.dirname(fullPath), { recursive: true }); + fs.writeFileSync(fullPath, value, "utf8"); +} + +afterEach(() => { + for (const dir of tempDirs.splice(0, tempDirs.length)) { + fs.rmSync(dir, { recursive: true, force: true }); + } +}); it("loads the plugin-entry runtime wrapper through native ESM import", async () => { const wrapperPath = path.join( @@ -20,3 +53,56 @@ it("loads the plugin-entry runtime wrapper through native ESM import", async () handleVerificationStatus: expect.any(Function), }); }, 240_000); + +it("loads the packaged runtime wrapper without recursing through the stable root alias", async () => { + const fixtureRoot = makeFixtureRoot(".tmp-matrix-runtime-"); + const wrapperSource = fs.readFileSync( + path.join(REPO_ROOT, "extensions", "matrix", "src", "plugin-entry.runtime.js"), + "utf8", + ); + + writeFixtureFile( + fixtureRoot, + "package.json", + JSON.stringify( + { + name: "openclaw", + type: "module", + exports: { + "./plugin-sdk": "./dist/plugin-sdk/index.js", + }, + }, + null, + 2, + ) + "\n", + ); + writeFixtureFile(fixtureRoot, "dist/plugin-sdk/index.js", "export {};\n"); + writeFixtureFile( + fixtureRoot, + "node_modules/jiti/index.js", + `module.exports = require(${JSON.stringify(JITI_ENTRY_PATH)});\n`, + ); + writeFixtureFile(fixtureRoot, "dist/plugin-entry.runtime-C88YIa_v.js", wrapperSource); + writeFixtureFile( + fixtureRoot, + "dist/plugin-entry.runtime.js", + 'export * from "./plugin-entry.runtime-C88YIa_v.js";\n', + ); + writeFixtureFile( + fixtureRoot, + "dist/extensions/matrix/plugin-entry.handlers.runtime.js", + PACKAGED_RUNTIME_STUB, + ); + + const wrapperUrl = pathToFileURL( + path.join(fixtureRoot, "dist", "plugin-entry.runtime-C88YIa_v.js"), + ); + const mod = await import(`${wrapperUrl.href}?t=${Date.now()}`); + + expect(mod).toMatchObject({ + ensureMatrixCryptoRuntime: expect.any(Function), + handleVerifyRecoveryKey: expect.any(Function), + handleVerificationBootstrap: expect.any(Function), + handleVerificationStatus: expect.any(Function), + }); +}, 240_000); diff --git a/test/scripts/bundled-plugin-build-entries.test.ts b/test/scripts/bundled-plugin-build-entries.test.ts index 765fc362af2..d7cda2ae873 100644 --- a/test/scripts/bundled-plugin-build-entries.test.ts +++ b/test/scripts/bundled-plugin-build-entries.test.ts @@ -19,6 +19,15 @@ describe("bundled plugin build entries", () => { }); }); + it("keeps the Matrix packaged runtime shim in bundled plugin build entries", () => { + const entries = listBundledPluginBuildEntries(); + + expect(entries).toMatchObject({ + "extensions/matrix/plugin-entry.handlers.runtime": + "extensions/matrix/plugin-entry.handlers.runtime.ts", + }); + }); + it("packs runtime core support packages without requiring plugin manifests", () => { const artifacts = listBundledPluginPackArtifacts(); @@ -32,4 +41,10 @@ describe("bundled plugin build entries", () => { expect(artifacts).toContain("dist/extensions/speech-core/runtime-api.js"); expect(artifacts).not.toContain("dist/extensions/speech-core/openclaw.plugin.json"); }); + + it("packs the Matrix packaged runtime shim", () => { + const artifacts = listBundledPluginPackArtifacts(); + + expect(artifacts).toContain("dist/extensions/matrix/plugin-entry.handlers.runtime.js"); + }); });