diff --git a/CHANGELOG.md b/CHANGELOG.md index 35e9e64c83e..82104e2c440 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -543,6 +543,7 @@ Docs: https://docs.openclaw.ai - Slack: route native stream fallback replies through the normal chunked sender so long buffered Slack Connect responses are not dropped or duplicated. (#71124) Thanks @martingarramon. - WhatsApp: transcribe accepted voice notes before agent dispatch while keeping spoken transcripts out of command authorization. (#64120) Thanks @rogerdigital. - Plugins/CLI: expose channel plugin CLI descriptors during discovery-mode plugin loads so snapshot registries keep channel commands visible without activating full runtimes. (#71309) Thanks @gumadeiras. +- Matrix: separate recovery-key, backup, and owner-trust diagnostics during E2EE recovery, add recovery-key rotation for backup reset, and cover destructive backup restore paths in QA. (#71311) Thanks @gumadeiras. - WhatsApp: deliver media generated by tool-result replies while still suppressing text-only tool chatter. (#60968) Thanks @adaclaw. - Config/agents: accept `agents.list[].contextTokens` in strict config validation so per-agent overrides survive hot reload, letting `/status` reflect the configured model window instead of the 200k fallback. Fixes #70692. (#71247) Thanks @statxc. - Heartbeat: include async exec completion details in heartbeat prompts so command-finished notifications relay the actual output. (#71213) Thanks @GodsBoy. diff --git a/extensions/matrix/src/matrix/sdk.test.ts b/extensions/matrix/src/matrix/sdk.test.ts index c250bee555a..5afa07b896e 100644 --- a/extensions/matrix/src/matrix/sdk.test.ts +++ b/extensions/matrix/src/matrix/sdk.test.ts @@ -1579,6 +1579,36 @@ describe("MatrixClient crypto bootstrapping", () => { expect(status.serverDeviceKnown).toBe(false); }); + it("keeps verification diagnostics when the homeserver device list cannot be read", async () => { + matrixJsClient.getUserId = vi.fn(() => "@bot:example.org"); + matrixJsClient.getDeviceId = vi.fn(() => "DEVICE123"); + matrixJsClient.getDevices = vi.fn(async () => { + throw new Error("device list unavailable"); + }); + matrixJsClient.getCrypto = vi.fn(() => ({ + on: vi.fn(), + bootstrapCrossSigning: vi.fn(async () => {}), + bootstrapSecretStorage: vi.fn(async () => {}), + requestOwnUserVerification: vi.fn(async () => null), + getDeviceVerificationStatus: vi.fn(async () => ({ + isVerified: () => true, + localVerified: true, + crossSigningVerified: true, + signedByOwner: true, + })), + })); + + const client = new MatrixClient("https://matrix.example.org", "token", { + encryption: true, + }); + await client.start(); + + const status = await client.getOwnDeviceVerificationStatus(); + expect(status.verified).toBe(true); + expect(status.backup).toBeDefined(); + expect(status.serverDeviceKnown).toBeNull(); + }); + it("does not treat local-only trust as Matrix identity trust", async () => { matrixJsClient.getUserId = vi.fn(() => "@bot:example.org"); matrixJsClient.getDeviceId = vi.fn(() => "DEVICE123"); diff --git a/extensions/matrix/src/matrix/sdk.ts b/extensions/matrix/src/matrix/sdk.ts index 471d1ae5cf9..f340afd58b4 100644 --- a/extensions/matrix/src/matrix/sdk.ts +++ b/extensions/matrix/src/matrix/sdk.ts @@ -1130,10 +1130,10 @@ export class MatrixClient { const [backup, deviceVerification, ownDevices] = await Promise.all([ this.getRoomKeyBackupStatus(), this.getDeviceVerificationStatus(userId, deviceId), - this.listOwnDevices(), + this.listOwnDevices().catch(() => null), ]); const serverDeviceKnown = deviceId - ? ownDevices.some((device) => device.deviceId === deviceId) + ? (ownDevices?.some((device) => device.deviceId === deviceId) ?? null) : null; return { diff --git a/extensions/qa-matrix/src/runners/contract/scenario-runtime-e2ee-destructive.ts b/extensions/qa-matrix/src/runners/contract/scenario-runtime-e2ee-destructive.ts index 06baa53c07f..7e2ed3405c5 100644 --- a/extensions/qa-matrix/src/runners/contract/scenario-runtime-e2ee-destructive.ts +++ b/extensions/qa-matrix/src/runners/contract/scenario-runtime-e2ee-destructive.ts @@ -1,5 +1,5 @@ import { randomUUID } from "node:crypto"; -import { copyFile, mkdir, readdir, readFile, rm, stat, writeFile } from "node:fs/promises"; +import { chmod, copyFile, mkdir, readdir, readFile, rm, stat, writeFile } from "node:fs/promises"; import path from "node:path"; import { setTimeout as sleep } from "node:timers/promises"; import type { MatrixVerificationSummary } from "@openclaw/matrix/test-api.js"; @@ -15,6 +15,7 @@ import { } from "./scenario-catalog.js"; import { createMatrixQaOpenClawCliRuntime, + formatMatrixQaCliCommand, redactMatrixQaCliOutput, type MatrixQaCliRunResult, } from "./scenario-runtime-cli.js"; @@ -273,13 +274,13 @@ function parseMatrixQaCliJson(result: MatrixQaCliRunResult): unknown { const stderr = result.stderr.trim(); const payload = stdout || stderr; if (!payload) { - throw new Error(`openclaw ${result.args.join(" ")} did not print JSON`); + throw new Error(`${formatMatrixQaCliCommand(result.args)} did not print JSON`); } try { return JSON.parse(payload) as unknown; } catch (error) { throw new Error( - `openclaw ${result.args.join(" ")} printed invalid JSON: ${ + `${formatMatrixQaCliCommand(result.args)} printed invalid JSON: ${ error instanceof Error ? error.message : String(error) }\n${redactMatrixQaCliOutput(payload)}`, { cause: error }, @@ -603,10 +604,14 @@ async function mutateMatrixQaCliStateLoss(params: { }) { const accountRoot = await findMatrixQaCliAccountRoot(params); const recoveryKeyPath = path.join(accountRoot, "recovery-key.json"); - const preservedRecoveryKeyPath = path.join(params.runtime.rootDir, "preserved-recovery-key.json"); + const preservedRecoveryKeyPath = path.join( + params.runtime.stateDir, + "preserved-recovery-key.json", + ); let recoveryKeyPreserved = false; if (params.preserveRecoveryKey) { await copyFile(recoveryKeyPath, preservedRecoveryKeyPath); + await chmod(preservedRecoveryKeyPath, 0o600).catch(() => undefined); recoveryKeyPreserved = true; } await rm(accountRoot, { force: true, recursive: true });