diff --git a/CHANGELOG.md b/CHANGELOG.md index 209e08daf71..642fd1affa8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,6 +51,7 @@ Docs: https://docs.openclaw.ai - Browser/plugins: auto-enable the bundled browser plugin when browser config or browser tool policy already references it, and show a clearer CLI error when `plugins.allow` excludes `browser`. - Matrix/plugin loading: ship and source-load the crypto bootstrap runtime sidecar correctly so current `main` stops warning about failed Matrix bootstrap loads and `matrix/index` plugin-id mismatches on every invocation. (#53298) thanks @keithce. - iOS/Live Activities: mark the `ActivityKit` import in `LiveActivityManager.swift` as `@preconcurrency` so Xcode 26.4 / Swift 6 builds stop failing on strict concurrency checks. (#57180) Thanks @ngutman. +- Plugins/Matrix: mirror the Matrix crypto WASM runtime dependency into the root packaged install and enforce root/plugin dependency parity so bundled Matrix E2EE crypto resolves correctly in shipped builds. (#57163) Thanks @gumadeiras. ## 2026.3.28 diff --git a/extensions/matrix/package.json b/extensions/matrix/package.json index 74f83ff6ffe..2ace082d928 100644 --- a/extensions/matrix/package.json +++ b/extensions/matrix/package.json @@ -5,6 +5,7 @@ "type": "module", "dependencies": { "@matrix-org/matrix-sdk-crypto-nodejs": "^0.4.0", + "@matrix-org/matrix-sdk-crypto-wasm": "18.0.0", "fake-indexeddb": "^6.2.5", "markdown-it": "14.1.1", "matrix-js-sdk": "41.2.0", @@ -44,9 +45,9 @@ }, "releaseChecks": { "rootDependencyMirrorAllowlist": [ + "@matrix-org/matrix-sdk-crypto-wasm", "@matrix-org/matrix-sdk-crypto-nodejs", - "matrix-js-sdk", - "music-metadata" + "matrix-js-sdk" ] } } diff --git a/extensions/matrix/src/matrix/deps.ts b/extensions/matrix/src/matrix/deps.ts index ef9c4514bc3..db21d4777ab 100644 --- a/extensions/matrix/src/matrix/deps.ts +++ b/extensions/matrix/src/matrix/deps.ts @@ -5,7 +5,11 @@ import path from "node:path"; import { fileURLToPath } from "node:url"; import type { RuntimeEnv } from "../runtime-api.js"; -const REQUIRED_MATRIX_PACKAGES = ["matrix-js-sdk", "@matrix-org/matrix-sdk-crypto-nodejs"]; +const REQUIRED_MATRIX_PACKAGES = [ + "matrix-js-sdk", + "@matrix-org/matrix-sdk-crypto-nodejs", + "@matrix-org/matrix-sdk-crypto-wasm", +]; type MatrixCryptoRuntimeDeps = { requireFn?: (id: string) => unknown; @@ -184,11 +188,11 @@ export async function ensureMatrixSdkInstalled(params: { const confirm = params.confirm; if (confirm) { const ok = await confirm( - "Matrix requires matrix-js-sdk and @matrix-org/matrix-sdk-crypto-nodejs. Install now?", + "Matrix requires matrix-js-sdk, @matrix-org/matrix-sdk-crypto-nodejs, and @matrix-org/matrix-sdk-crypto-wasm. Install now?", ); if (!ok) { throw new Error( - "Matrix requires matrix-js-sdk and @matrix-org/matrix-sdk-crypto-nodejs (install dependencies first).", + "Matrix requires matrix-js-sdk, @matrix-org/matrix-sdk-crypto-nodejs, and @matrix-org/matrix-sdk-crypto-wasm (install dependencies first).", ); } } diff --git a/extensions/matrix/src/onboarding.ts b/extensions/matrix/src/onboarding.ts index 72ecc2810a6..fbbe229a476 100644 --- a/extensions/matrix/src/onboarding.ts +++ b/extensions/matrix/src/onboarding.ts @@ -566,7 +566,7 @@ export const matrixOnboardingAdapter: MatrixOnboardingAdapter = { channel, configured: false, statusLines: ['Matrix: set "channels.matrix.defaultAccount" to select a named account'], - selectionHint: !sdkReady ? "install matrix-js-sdk" : "set defaultAccount", + selectionHint: !sdkReady ? "install Matrix deps" : "set defaultAccount", }; } const account = resolveMatrixAccount({ @@ -580,7 +580,7 @@ export const matrixOnboardingAdapter: MatrixOnboardingAdapter = { statusLines: [ `Matrix: ${configured ? "configured" : "needs homeserver + access token or password"}`, ], - selectionHint: !sdkReady ? "install matrix-js-sdk" : configured ? "configured" : "needs auth", + selectionHint: !sdkReady ? "install Matrix deps" : configured ? "configured" : "needs auth", }; }, configure: async ({ diff --git a/package.json b/package.json index ad364a326fb..2efd767ae22 100644 --- a/package.json +++ b/package.json @@ -1183,6 +1183,7 @@ "@mariozechner/pi-ai": "0.63.2", "@mariozechner/pi-coding-agent": "0.63.2", "@mariozechner/pi-tui": "0.63.2", + "@matrix-org/matrix-sdk-crypto-wasm": "18.0.0", "@modelcontextprotocol/sdk": "1.28.0", "@mozilla/readability": "^0.6.0", "@openclaw/plugin-package-contract": "workspace:*", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2069571ab96..c80e1b163f4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -60,6 +60,9 @@ importers: '@mariozechner/pi-tui': specifier: 0.63.2 version: 0.63.2 + '@matrix-org/matrix-sdk-crypto-wasm': + specifier: 18.0.0 + version: 18.0.0 '@modelcontextprotocol/sdk': specifier: 1.28.0 version: 1.28.0(zod@4.3.6) @@ -259,6 +262,8 @@ importers: extensions/anthropic: {} + extensions/anthropic-vertex: {} + extensions/bluebubbles: devDependencies: openclaw: @@ -436,6 +441,9 @@ importers: '@matrix-org/matrix-sdk-crypto-nodejs': specifier: ^0.4.0 version: 0.4.0 + '@matrix-org/matrix-sdk-crypto-wasm': + specifier: 18.0.0 + version: 18.0.0 fake-indexeddb: specifier: ^6.2.5 version: 6.2.5 diff --git a/scripts/lib/bundled-extension-manifest.ts b/scripts/lib/bundled-extension-manifest.ts index 673bf442313..d7fca5cbba7 100644 --- a/scripts/lib/bundled-extension-manifest.ts +++ b/scripts/lib/bundled-extension-manifest.ts @@ -11,6 +11,7 @@ export type ExtensionPackageJson = { optionalDependencies?: Record; openclaw?: { install?: unknown; + releaseChecks?: unknown; }; }; diff --git a/scripts/release-check.ts b/scripts/release-check.ts index 618e6901254..0b39aa1410a 100755 --- a/scripts/release-check.ts +++ b/scripts/release-check.ts @@ -60,18 +60,95 @@ function collectBundledExtensions(): BundledExtension[] { }); } +function collectRuntimeDependencySpecs(packageJson: { + dependencies?: Record; + optionalDependencies?: Record; +}): Map { + return new Map([ + ...Object.entries(packageJson.dependencies ?? {}), + ...Object.entries(packageJson.optionalDependencies ?? {}), + ]); +} + function checkBundledExtensionMetadata() { const extensions = collectBundledExtensions(); const manifestErrors = collectBundledExtensionManifestErrors(extensions); - if (manifestErrors.length > 0) { + const rootPackage = JSON.parse(readFileSync(resolve("package.json"), "utf8")) as { + dependencies?: Record; + optionalDependencies?: Record; + }; + const rootRuntimeDeps = collectRuntimeDependencySpecs(rootPackage); + const rootMirrorErrors = collectBundledExtensionRootDependencyMirrorErrors( + extensions, + rootRuntimeDeps, + ); + const errors = [...manifestErrors, ...rootMirrorErrors]; + if (errors.length > 0) { console.error("release-check: bundled extension manifest validation failed:"); - for (const error of manifestErrors) { + for (const error of errors) { console.error(` - ${error}`); } process.exit(1); } } +export function collectBundledExtensionRootDependencyMirrorErrors( + extensions: BundledExtension[], + rootRuntimeDeps: ReadonlyMap, +): string[] { + const errors: string[] = []; + + for (const extension of extensions) { + const rawReleaseChecks = extension.packageJson.openclaw?.releaseChecks; + const allowlist = (rawReleaseChecks as { rootDependencyMirrorAllowlist?: unknown } | undefined) + ?.rootDependencyMirrorAllowlist; + + if (allowlist === undefined) { + continue; + } + if (!Array.isArray(allowlist)) { + errors.push( + `bundled extension '${extension.id}' manifest invalid | openclaw.releaseChecks.rootDependencyMirrorAllowlist must be an array`, + ); + continue; + } + + const extensionRuntimeDeps = collectRuntimeDependencySpecs(extension.packageJson); + + for (const entry of allowlist) { + if (typeof entry !== "string" || entry.trim().length === 0) { + errors.push( + `bundled extension '${extension.id}' manifest invalid | openclaw.releaseChecks.rootDependencyMirrorAllowlist entries must be non-empty strings`, + ); + continue; + } + + const extensionSpec = extensionRuntimeDeps.get(entry); + if (!extensionSpec) { + errors.push( + `bundled extension '${extension.id}' manifest invalid | openclaw.releaseChecks.rootDependencyMirrorAllowlist entry '${entry}' must be declared in extension runtime dependencies`, + ); + } + const rootSpec = rootRuntimeDeps.get(entry); + if (!rootSpec) { + errors.push( + `bundled extension '${extension.id}' manifest invalid | openclaw.releaseChecks.rootDependencyMirrorAllowlist entry '${entry}' must be mirrored in root runtime dependencies`, + ); + } + if (!extensionSpec || !rootSpec) { + continue; + } + if (extensionSpec !== rootSpec) { + errors.push( + `bundled extension '${extension.id}' manifest invalid | openclaw.releaseChecks.rootDependencyMirrorAllowlist entry '${entry}' must match root runtime dependency version (extension '${extensionSpec}', root '${rootSpec}')`, + ); + } + } + } + + return errors; +} + function runPackDry(): PackResult[] { const raw = execSync("npm pack --dry-run --json --ignore-scripts", { encoding: "utf8", diff --git a/src/plugin-sdk/package-contract-guardrails.test.ts b/src/plugin-sdk/package-contract-guardrails.test.ts index 660f033dd1d..05dd5fdb0ca 100644 --- a/src/plugin-sdk/package-contract-guardrails.test.ts +++ b/src/plugin-sdk/package-contract-guardrails.test.ts @@ -1,6 +1,9 @@ -import { readFileSync } from "node:fs"; -import { dirname, resolve } from "node:path"; -import { fileURLToPath } from "node:url"; +import { execFileSync } from "node:child_process"; +import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { createRequire } from "node:module"; +import os from "node:os"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; import { describe, expect, it } from "vitest"; import { pluginSdkEntrypoints } from "./entrypoints.js"; @@ -56,6 +59,62 @@ function readRootPackageJson(): { }; } +function readMatrixPackageJson(): { + dependencies?: Record; + optionalDependencies?: Record; + openclaw?: { + releaseChecks?: { + rootDependencyMirrorAllowlist?: unknown; + }; + }; +} { + return JSON.parse(readFileSync(resolve(REPO_ROOT, "extensions/matrix/package.json"), "utf8")) as { + dependencies?: Record; + optionalDependencies?: Record; + openclaw?: { + releaseChecks?: { + rootDependencyMirrorAllowlist?: unknown; + }; + }; + }; +} + +function collectRuntimeDependencySpecs(packageJson: { + dependencies?: Record; + optionalDependencies?: Record; +}): Map { + return new Map([ + ...Object.entries(packageJson.dependencies ?? {}), + ...Object.entries(packageJson.optionalDependencies ?? {}), + ]); +} + +function createRootPackageRequire() { + return createRequire(pathToFileURL(resolve(REPO_ROOT, "package.json")).href); +} + +function resolvePackageManagerCommand(name: "npm" | "pnpm"): string { + return process.platform === "win32" ? `${name}.cmd` : name; +} + +function packOpenClawToTempDir(packDir: string): string { + const raw = execFileSync( + resolvePackageManagerCommand("npm"), + ["pack", "--ignore-scripts", "--json", "--pack-destination", packDir], + { + cwd: REPO_ROOT, + encoding: "utf8", + env: { ...process.env, COREPACK_ENABLE_DOWNLOAD_PROMPT: "0" }, + }, + ); + const parsed = JSON.parse(raw) as Array<{ filename?: string }>; + const filename = parsed[0]?.filename?.trim(); + if (!filename) { + throw new Error(`npm pack did not return a filename: ${raw}`); + } + return join(packDir, filename); +} + function readGeneratedFacadeTypeMap(): string { return readFileSync( resolve(REPO_ROOT, "src/generated/plugin-sdk-facade-type-map.generated.ts"), @@ -97,10 +156,77 @@ describe("plugin-sdk package contract guardrails", () => { }); it("mirrors matrix runtime deps needed by the bundled host graph", () => { - const { dependencies = {}, optionalDependencies = {} } = readRootPackageJson(); + const rootRuntimeDeps = collectRuntimeDependencySpecs(readRootPackageJson()); + const matrixPackageJson = readMatrixPackageJson(); + const matrixRuntimeDeps = collectRuntimeDependencySpecs(matrixPackageJson); + const allowlist = matrixPackageJson.openclaw?.releaseChecks?.rootDependencyMirrorAllowlist; - expect(dependencies["matrix-js-sdk"]).toBe("41.2.0"); - expect(optionalDependencies["@matrix-org/matrix-sdk-crypto-nodejs"]).toBe("^0.4.0"); + expect(Array.isArray(allowlist)).toBe(true); + const matrixRootMirrorAllowlist = allowlist as string[]; + expect(matrixRootMirrorAllowlist).toEqual( + expect.arrayContaining(["@matrix-org/matrix-sdk-crypto-wasm"]), + ); + + for (const dep of matrixRootMirrorAllowlist) { + expect(rootRuntimeDeps.get(dep)).toBe(matrixRuntimeDeps.get(dep)); + } + }); + + it("resolves matrix crypto WASM from the root runtime surface", () => { + const rootRequire = createRootPackageRequire(); + + expect(rootRequire.resolve("@matrix-org/matrix-sdk-crypto-wasm")).toContain( + "@matrix-org/matrix-sdk-crypto-wasm", + ); + }); + + it("resolves matrix crypto WASM from an installed packed artifact", () => { + const tempRoot = mkdtempSync(join(os.tmpdir(), "openclaw-matrix-wasm-pack-")); + try { + const packDir = join(tempRoot, "pack"); + const consumerDir = join(tempRoot, "consumer"); + mkdirSync(packDir, { recursive: true }); + mkdirSync(consumerDir, { recursive: true }); + writeFileSync( + join(consumerDir, "package.json"), + `${JSON.stringify({ name: "matrix-wasm-smoke", private: true }, null, 2)}\n`, + "utf8", + ); + + const archivePath = packOpenClawToTempDir(packDir); + + execFileSync( + resolvePackageManagerCommand("pnpm"), + ["add", "--offline", "--ignore-scripts", archivePath], + { + cwd: consumerDir, + encoding: "utf8", + env: { ...process.env, COREPACK_ENABLE_DOWNLOAD_PROMPT: "0" }, + stdio: ["ignore", "pipe", "pipe"], + }, + ); + + const installedPackageJsonPath = join( + consumerDir, + "node_modules", + "openclaw", + "package.json", + ); + const installedPackageJson = JSON.parse(readFileSync(installedPackageJsonPath, "utf8")) as { + dependencies?: Record; + }; + const installedRequire = createRequire(pathToFileURL(installedPackageJsonPath).href); + const matrixPackageJson = readMatrixPackageJson(); + + expect(installedPackageJson.dependencies?.["@matrix-org/matrix-sdk-crypto-wasm"]).toBe( + matrixPackageJson.dependencies?.["@matrix-org/matrix-sdk-crypto-wasm"], + ); + expect(installedRequire.resolve("@matrix-org/matrix-sdk-crypto-wasm")).toContain( + "@matrix-org/matrix-sdk-crypto-wasm", + ); + } finally { + rmSync(tempRoot, { recursive: true, force: true }); + } }); it("keeps generated facade types on package-style module specifiers", () => { diff --git a/test/release-check.test.ts b/test/release-check.test.ts index e0e6bd17b06..98034974e54 100644 --- a/test/release-check.test.ts +++ b/test/release-check.test.ts @@ -4,6 +4,7 @@ import { listPluginSdkDistArtifacts } from "../scripts/lib/plugin-sdk-entries.mj import { collectAppcastSparkleVersionErrors, collectBundledExtensionManifestErrors, + collectBundledExtensionRootDependencyMirrorErrors, collectForbiddenPackPaths, collectMissingPackPaths, collectPackUnpackedSizeErrors, @@ -109,6 +110,128 @@ describe("collectBundledExtensionManifestErrors", () => { }); }); +describe("collectBundledExtensionRootDependencyMirrorErrors", () => { + it("flags a non-array mirror allowlist", () => { + expect( + collectBundledExtensionRootDependencyMirrorErrors( + [ + { + id: "matrix", + packageJson: { + openclaw: { + releaseChecks: { + rootDependencyMirrorAllowlist: true, + }, + }, + }, + }, + ], + new Map(), + ), + ).toEqual([ + "bundled extension 'matrix' manifest invalid | openclaw.releaseChecks.rootDependencyMirrorAllowlist must be an array", + ]); + }); + + it("flags mirror entries missing from extension runtime dependencies", () => { + expect( + collectBundledExtensionRootDependencyMirrorErrors( + [ + { + id: "matrix", + packageJson: { + dependencies: { + "matrix-js-sdk": "41.2.0", + }, + openclaw: { + releaseChecks: { + rootDependencyMirrorAllowlist: ["@matrix-org/matrix-sdk-crypto-wasm"], + }, + }, + }, + }, + ], + new Map([["@matrix-org/matrix-sdk-crypto-wasm", "18.0.0"]]), + ), + ).toEqual([ + "bundled extension 'matrix' manifest invalid | openclaw.releaseChecks.rootDependencyMirrorAllowlist entry '@matrix-org/matrix-sdk-crypto-wasm' must be declared in extension runtime dependencies", + ]); + }); + + it("flags mirror entries missing from root runtime dependencies", () => { + expect( + collectBundledExtensionRootDependencyMirrorErrors( + [ + { + id: "matrix", + packageJson: { + dependencies: { + "@matrix-org/matrix-sdk-crypto-wasm": "18.0.0", + }, + openclaw: { + releaseChecks: { + rootDependencyMirrorAllowlist: ["@matrix-org/matrix-sdk-crypto-wasm"], + }, + }, + }, + }, + ], + new Map(), + ), + ).toEqual([ + "bundled extension 'matrix' manifest invalid | openclaw.releaseChecks.rootDependencyMirrorAllowlist entry '@matrix-org/matrix-sdk-crypto-wasm' must be mirrored in root runtime dependencies", + ]); + }); + + it("flags mirror entries whose root version drifts from the extension", () => { + expect( + collectBundledExtensionRootDependencyMirrorErrors( + [ + { + id: "matrix", + packageJson: { + dependencies: { + "@matrix-org/matrix-sdk-crypto-wasm": "18.0.0", + }, + openclaw: { + releaseChecks: { + rootDependencyMirrorAllowlist: ["@matrix-org/matrix-sdk-crypto-wasm"], + }, + }, + }, + }, + ], + new Map([["@matrix-org/matrix-sdk-crypto-wasm", "18.1.0"]]), + ), + ).toEqual([ + "bundled extension 'matrix' manifest invalid | openclaw.releaseChecks.rootDependencyMirrorAllowlist entry '@matrix-org/matrix-sdk-crypto-wasm' must match root runtime dependency version (extension '18.0.0', root '18.1.0')", + ]); + }); + + it("accepts mirror entries declared by both the extension and root package", () => { + expect( + collectBundledExtensionRootDependencyMirrorErrors( + [ + { + id: "matrix", + packageJson: { + dependencies: { + "@matrix-org/matrix-sdk-crypto-wasm": "18.0.0", + }, + openclaw: { + releaseChecks: { + rootDependencyMirrorAllowlist: ["@matrix-org/matrix-sdk-crypto-wasm"], + }, + }, + }, + }, + ], + new Map([["@matrix-org/matrix-sdk-crypto-wasm", "18.0.0"]]), + ), + ).toEqual([]); + }); +}); + describe("collectForbiddenPackPaths", () => { it("allows bundled plugin runtime deps under dist/extensions but still blocks other node_modules", () => { expect(