mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-12 01:31:08 +00:00
Merged via squash.
Prepared head SHA: b0495dc6ca
Co-authored-by: MoerAI <26067127+MoerAI@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
This commit is contained in:
@@ -4,6 +4,8 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
## Unreleased
|
||||
|
||||
### Fixes
|
||||
|
||||
## 2026.4.5
|
||||
|
||||
### Breaking
|
||||
@@ -244,6 +246,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Plugins/facades: resolve globally installed bundled-plugin runtime facades from registry roots so bundled channels like LINE still boot when the winning plugin install lives under the global extensions directory with an encoded scoped folder name. (#61297) Thanks @openperf.
|
||||
- Matrix: avoid failing startup when token auth already knows the user ID but still needs optional device metadata, retry transient auth bootstrap requests, and backfill missing device IDs after startup while keeping unknown-device storage reuse conservative until metadata is repaired. (#61383) Thanks @gumadeiras.
|
||||
- Agents/exec: stop streaming `tool_execution_update` events after an exec session backgrounds, preventing delayed background output from hitting a stale listener and crashing the gateway while keeping the output available through `process poll/log`. (#61627) Thanks @openperf.
|
||||
- Matrix: pass configured `deviceId` through health probes and keep probe-only client setup out of durable Matrix storage, so health checks preserve the correct device identity without rewriting `storage-meta.json` or related probe state on disk. (#61581) Thanks @MoerAI.
|
||||
|
||||
## 2026.4.2
|
||||
|
||||
|
||||
@@ -49,6 +49,7 @@ describe("matrix account path propagation", () => {
|
||||
homeserver: "https://matrix.example.org",
|
||||
userId: "@poe:example.org",
|
||||
accessToken: "poe-token",
|
||||
deviceId: "POEDEVICE",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -66,7 +67,7 @@ describe("matrix account path propagation", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("forwards accountId to matrix probes", async () => {
|
||||
it("forwards accountId and deviceId to matrix probes", async () => {
|
||||
await matrixPlugin.status!.probeAccount?.({
|
||||
cfg: {} as never,
|
||||
timeoutMs: 500,
|
||||
@@ -83,6 +84,7 @@ describe("matrix account path propagation", () => {
|
||||
homeserver: "https://matrix.example.org",
|
||||
accessToken: "poe-token",
|
||||
userId: "@poe:example.org",
|
||||
deviceId: "POEDEVICE",
|
||||
timeoutMs: 500,
|
||||
accountId: "poe",
|
||||
});
|
||||
|
||||
@@ -498,6 +498,7 @@ export const matrixPlugin: ChannelPlugin<ResolvedMatrixAccount, MatrixProbe> =
|
||||
homeserver: auth.homeserver,
|
||||
accessToken: auth.accessToken,
|
||||
userId: auth.userId,
|
||||
deviceId: auth.deviceId,
|
||||
timeoutMs,
|
||||
accountId: account.accountId,
|
||||
allowPrivateNetwork: auth.allowPrivateNetwork,
|
||||
|
||||
115
extensions/matrix/src/matrix/client/create-client.test.ts
Normal file
115
extensions/matrix/src/matrix/client/create-client.test.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const ensureMatrixSdkLoggingConfiguredMock = vi.hoisted(() => vi.fn());
|
||||
const resolveValidatedMatrixHomeserverUrlMock = vi.hoisted(() => vi.fn());
|
||||
const maybeMigrateLegacyStorageMock = vi.hoisted(() => vi.fn(async () => undefined));
|
||||
const resolveMatrixStoragePathsMock = vi.hoisted(() => vi.fn());
|
||||
const writeStorageMetaMock = vi.hoisted(() => vi.fn());
|
||||
const MatrixClientMock = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock("./logging.js", () => ({
|
||||
ensureMatrixSdkLoggingConfigured: ensureMatrixSdkLoggingConfiguredMock,
|
||||
}));
|
||||
|
||||
vi.mock("./config.js", () => ({
|
||||
resolveValidatedMatrixHomeserverUrl: resolveValidatedMatrixHomeserverUrlMock,
|
||||
}));
|
||||
|
||||
vi.mock("./storage.js", () => ({
|
||||
maybeMigrateLegacyStorage: maybeMigrateLegacyStorageMock,
|
||||
resolveMatrixStoragePaths: resolveMatrixStoragePathsMock,
|
||||
writeStorageMeta: writeStorageMetaMock,
|
||||
}));
|
||||
|
||||
vi.mock("../sdk.js", () => ({
|
||||
MatrixClient: MatrixClientMock,
|
||||
}));
|
||||
|
||||
let createMatrixClient: typeof import("./create-client.js").createMatrixClient;
|
||||
|
||||
describe("createMatrixClient", () => {
|
||||
const storagePaths = {
|
||||
rootDir: "/tmp/openclaw-matrix-create-client-test",
|
||||
storagePath: "/tmp/openclaw-matrix-create-client-test/storage.json",
|
||||
recoveryKeyPath: "/tmp/openclaw-matrix-create-client-test/recovery.key",
|
||||
idbSnapshotPath: "/tmp/openclaw-matrix-create-client-test/idb.snapshot",
|
||||
metaPath: "/tmp/openclaw-matrix-create-client-test/storage-meta.json",
|
||||
accountKey: "default",
|
||||
tokenHash: "token-hash",
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
({ createMatrixClient } = await import("./create-client.js"));
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
ensureMatrixSdkLoggingConfiguredMock.mockReturnValue(undefined);
|
||||
resolveValidatedMatrixHomeserverUrlMock.mockResolvedValue("https://matrix.example.org");
|
||||
resolveMatrixStoragePathsMock.mockReturnValue(storagePaths);
|
||||
MatrixClientMock.mockImplementation(function MockMatrixClient() {
|
||||
return {
|
||||
stop: vi.fn(),
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
it("persists storage metadata by default", async () => {
|
||||
await createMatrixClient({
|
||||
homeserver: "https://matrix.example.org",
|
||||
userId: "@bot:example.org",
|
||||
accessToken: "tok",
|
||||
});
|
||||
|
||||
expect(writeStorageMetaMock).toHaveBeenCalledWith({
|
||||
storagePaths,
|
||||
homeserver: "https://matrix.example.org",
|
||||
userId: "@bot:example.org",
|
||||
accountId: undefined,
|
||||
deviceId: undefined,
|
||||
});
|
||||
expect(resolveMatrixStoragePathsMock).toHaveBeenCalledTimes(1);
|
||||
expect(MatrixClientMock).toHaveBeenCalledWith("https://matrix.example.org", "tok", {
|
||||
userId: "@bot:example.org",
|
||||
password: undefined,
|
||||
deviceId: undefined,
|
||||
encryption: undefined,
|
||||
localTimeoutMs: undefined,
|
||||
initialSyncLimit: undefined,
|
||||
storagePath: storagePaths.storagePath,
|
||||
recoveryKeyPath: storagePaths.recoveryKeyPath,
|
||||
idbSnapshotPath: storagePaths.idbSnapshotPath,
|
||||
cryptoDatabasePrefix: "openclaw-matrix-default-token-hash",
|
||||
autoBootstrapCrypto: undefined,
|
||||
ssrfPolicy: undefined,
|
||||
dispatcherPolicy: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("skips persistent storage wiring when persistence is disabled", async () => {
|
||||
await createMatrixClient({
|
||||
homeserver: "https://matrix.example.org",
|
||||
userId: "@bot:example.org",
|
||||
accessToken: "tok",
|
||||
persistStorage: false,
|
||||
});
|
||||
|
||||
expect(resolveMatrixStoragePathsMock).not.toHaveBeenCalled();
|
||||
expect(writeStorageMetaMock).not.toHaveBeenCalled();
|
||||
expect(MatrixClientMock).toHaveBeenCalledWith("https://matrix.example.org", "tok", {
|
||||
userId: "@bot:example.org",
|
||||
password: undefined,
|
||||
deviceId: undefined,
|
||||
encryption: undefined,
|
||||
localTimeoutMs: undefined,
|
||||
initialSyncLimit: undefined,
|
||||
storagePath: undefined,
|
||||
recoveryKeyPath: undefined,
|
||||
idbSnapshotPath: undefined,
|
||||
cryptoDatabasePrefix: undefined,
|
||||
autoBootstrapCrypto: undefined,
|
||||
ssrfPolicy: undefined,
|
||||
dispatcherPolicy: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -33,6 +33,7 @@ export async function createMatrixClient(params: {
|
||||
accessToken: string;
|
||||
password?: string;
|
||||
deviceId?: string;
|
||||
persistStorage?: boolean;
|
||||
encryption?: boolean;
|
||||
localTimeoutMs?: number;
|
||||
initialSyncLimit?: number;
|
||||
@@ -45,36 +46,41 @@ export async function createMatrixClient(params: {
|
||||
const { MatrixClient, ensureMatrixSdkLoggingConfigured } =
|
||||
await loadMatrixCreateClientRuntimeDeps();
|
||||
ensureMatrixSdkLoggingConfigured();
|
||||
const env = process.env;
|
||||
const homeserver = await resolveValidatedMatrixHomeserverUrl(params.homeserver, {
|
||||
dangerouslyAllowPrivateNetwork: params.allowPrivateNetwork,
|
||||
});
|
||||
const userId = params.userId?.trim() || "unknown";
|
||||
const matrixClientUserId = params.userId?.trim() || undefined;
|
||||
const persistStorage = params.persistStorage !== false;
|
||||
const storagePaths = persistStorage
|
||||
? resolveMatrixStoragePaths({
|
||||
homeserver,
|
||||
userId,
|
||||
accessToken: params.accessToken,
|
||||
accountId: params.accountId,
|
||||
deviceId: params.deviceId,
|
||||
env: process.env,
|
||||
})
|
||||
: null;
|
||||
|
||||
const storagePaths = resolveMatrixStoragePaths({
|
||||
homeserver,
|
||||
userId,
|
||||
accessToken: params.accessToken,
|
||||
accountId: params.accountId,
|
||||
deviceId: params.deviceId,
|
||||
env,
|
||||
});
|
||||
await maybeMigrateLegacyStorage({
|
||||
storagePaths,
|
||||
env,
|
||||
});
|
||||
fs.mkdirSync(storagePaths.rootDir, { recursive: true });
|
||||
if (storagePaths) {
|
||||
await maybeMigrateLegacyStorage({
|
||||
storagePaths,
|
||||
env: process.env,
|
||||
});
|
||||
fs.mkdirSync(storagePaths.rootDir, { recursive: true });
|
||||
writeStorageMeta({
|
||||
storagePaths,
|
||||
homeserver,
|
||||
userId,
|
||||
accountId: params.accountId,
|
||||
deviceId: params.deviceId,
|
||||
});
|
||||
}
|
||||
|
||||
writeStorageMeta({
|
||||
storagePaths,
|
||||
homeserver,
|
||||
userId,
|
||||
accountId: params.accountId,
|
||||
deviceId: params.deviceId,
|
||||
});
|
||||
|
||||
const cryptoDatabasePrefix = `openclaw-matrix-${storagePaths.accountKey}-${storagePaths.tokenHash}`;
|
||||
const cryptoDatabasePrefix = storagePaths
|
||||
? `openclaw-matrix-${storagePaths.accountKey}-${storagePaths.tokenHash}`
|
||||
: undefined;
|
||||
|
||||
return new MatrixClient(homeserver, params.accessToken, {
|
||||
userId: matrixClientUserId,
|
||||
@@ -83,9 +89,9 @@ export async function createMatrixClient(params: {
|
||||
encryption: params.encryption,
|
||||
localTimeoutMs: params.localTimeoutMs,
|
||||
initialSyncLimit: params.initialSyncLimit,
|
||||
storagePath: storagePaths.storagePath,
|
||||
recoveryKeyPath: storagePaths.recoveryKeyPath,
|
||||
idbSnapshotPath: storagePaths.idbSnapshotPath,
|
||||
storagePath: storagePaths?.storagePath,
|
||||
recoveryKeyPath: storagePaths?.recoveryKeyPath,
|
||||
idbSnapshotPath: storagePaths?.idbSnapshotPath,
|
||||
cryptoDatabasePrefix,
|
||||
autoBootstrapCrypto: params.autoBootstrapCrypto,
|
||||
ssrfPolicy: params.ssrfPolicy,
|
||||
|
||||
@@ -34,6 +34,7 @@ describe("probeMatrix", () => {
|
||||
homeserver: "https://matrix.example.org",
|
||||
userId: undefined,
|
||||
accessToken: "tok",
|
||||
persistStorage: false,
|
||||
localTimeoutMs: 1234,
|
||||
});
|
||||
});
|
||||
@@ -50,6 +51,7 @@ describe("probeMatrix", () => {
|
||||
homeserver: "https://matrix.example.org",
|
||||
userId: "@bot:example.org",
|
||||
accessToken: "tok",
|
||||
persistStorage: false,
|
||||
localTimeoutMs: 500,
|
||||
});
|
||||
});
|
||||
@@ -67,6 +69,7 @@ describe("probeMatrix", () => {
|
||||
homeserver: "https://matrix.example.org",
|
||||
userId: "@bot:example.org",
|
||||
accessToken: "tok",
|
||||
persistStorage: false,
|
||||
localTimeoutMs: 500,
|
||||
accountId: "ops",
|
||||
});
|
||||
@@ -87,6 +90,7 @@ describe("probeMatrix", () => {
|
||||
homeserver: "https://matrix.example.org",
|
||||
userId: undefined,
|
||||
accessToken: "tok",
|
||||
persistStorage: false,
|
||||
localTimeoutMs: 500,
|
||||
dispatcherPolicy: {
|
||||
mode: "explicit-proxy",
|
||||
@@ -95,6 +99,44 @@ describe("probeMatrix", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("passes deviceId through to client creation (#61317)", async () => {
|
||||
await probeMatrix({
|
||||
homeserver: "https://matrix.example.org",
|
||||
accessToken: "tok",
|
||||
userId: "@bot:example.org",
|
||||
deviceId: "ABCDEF",
|
||||
timeoutMs: 500,
|
||||
accountId: "ops",
|
||||
});
|
||||
|
||||
expect(createMatrixClientMock).toHaveBeenCalledWith({
|
||||
homeserver: "https://matrix.example.org",
|
||||
userId: "@bot:example.org",
|
||||
accessToken: "tok",
|
||||
deviceId: "ABCDEF",
|
||||
persistStorage: false,
|
||||
localTimeoutMs: 500,
|
||||
accountId: "ops",
|
||||
});
|
||||
});
|
||||
|
||||
it("omits deviceId when not provided", async () => {
|
||||
await probeMatrix({
|
||||
homeserver: "https://matrix.example.org",
|
||||
accessToken: "tok",
|
||||
timeoutMs: 500,
|
||||
});
|
||||
|
||||
expect(createMatrixClientMock).toHaveBeenCalledWith({
|
||||
homeserver: "https://matrix.example.org",
|
||||
userId: undefined,
|
||||
accessToken: "tok",
|
||||
deviceId: undefined,
|
||||
persistStorage: false,
|
||||
localTimeoutMs: 500,
|
||||
});
|
||||
});
|
||||
|
||||
it("returns client validation errors for insecure public http homeservers", async () => {
|
||||
createMatrixClientMock.mockRejectedValue(
|
||||
new Error("Matrix homeserver must use https:// unless it targets a private or loopback host"),
|
||||
|
||||
@@ -24,6 +24,7 @@ export async function probeMatrix(params: {
|
||||
homeserver: string;
|
||||
accessToken: string;
|
||||
userId?: string;
|
||||
deviceId?: string;
|
||||
timeoutMs: number;
|
||||
accountId?: string | null;
|
||||
allowPrivateNetwork?: boolean;
|
||||
@@ -65,6 +66,8 @@ export async function probeMatrix(params: {
|
||||
homeserver: params.homeserver,
|
||||
userId: inputUserId,
|
||||
accessToken: params.accessToken,
|
||||
deviceId: params.deviceId,
|
||||
persistStorage: false,
|
||||
localTimeoutMs: params.timeoutMs,
|
||||
accountId: params.accountId,
|
||||
allowPrivateNetwork: params.allowPrivateNetwork,
|
||||
|
||||
Reference in New Issue
Block a user