fix(matrix): package verification bootstrap runtime (#59249)

Merged via squash.

Prepared head SHA: df5891b663
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
This commit is contained in:
Gustavo Madeira Santana 2026-04-01 17:52:51 -04:00 committed by GitHub
parent c87c8e66bf
commit a204f790ce
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 151 additions and 4 deletions

View File

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

View File

@ -0,0 +1 @@
export * from "./src/plugin-entry.runtime.ts";

View File

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

View File

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

View File

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