fix: harden Matrix CLI verification artifacts

This commit is contained in:
Gustavo Madeira Santana
2026-04-23 00:04:06 -04:00
parent b618b9b428
commit 2079e1ca1b
4 changed files with 319 additions and 214 deletions

View File

@@ -288,10 +288,10 @@ describe("matrix CLI verification commands", () => {
expect(consoleLogMock).toHaveBeenCalledWith("Device verified by owner: no");
expect(consoleLogMock).toHaveBeenCalledWith("Backup: active and trusted on this device");
expect(consoleLogMock).toHaveBeenCalledWith(
"- Recovery key can unlock the room-key backup, but full Matrix identity trust is still incomplete. Run 'openclaw matrix verify self' and follow the prompts from another Matrix client.",
"- Recovery key can unlock the room-key backup, but full Matrix identity trust is still incomplete. Run openclaw matrix verify self and follow the prompts from another Matrix client.",
);
expect(consoleLogMock).toHaveBeenCalledWith(
"- If you intend to replace the current cross-signing identity, run 'openclaw matrix verify bootstrap --recovery-key <key> --force-reset-cross-signing'.",
"- If you intend to replace the current cross-signing identity, run openclaw matrix verify bootstrap --recovery-key '<key>' --force-reset-cross-signing.",
);
});
@@ -378,13 +378,13 @@ describe("matrix CLI verification commands", () => {
"- Accept the verification request in another Matrix client for this account.",
);
expect(consoleLogMock).toHaveBeenCalledWith(
"- Then run 'openclaw matrix verify start txn-1 --account ops' to start SAS verification.",
"- Then run openclaw matrix verify start txn-1 --account ops to start SAS verification.",
);
expect(consoleLogMock).toHaveBeenCalledWith(
"- Run 'openclaw matrix verify sas txn-1 --account ops' to display the SAS emoji or decimals.",
"- Run openclaw matrix verify sas txn-1 --account ops to display the SAS emoji or decimals.",
);
expect(consoleLogMock).toHaveBeenCalledWith(
"- When the SAS matches, run 'openclaw matrix verify confirm-sas txn-1 --account ops'.",
"- When the SAS matches, run openclaw matrix verify confirm-sas txn-1 --account ops.",
);
});
@@ -420,19 +420,19 @@ describe("matrix CLI verification commands", () => {
listMatrixVerificationsMock.mockResolvedValue([
mockMatrixVerificationSummary({
id: "self-\u001B[31m1",
transactionId: "txn-\n1",
otherUserId: "@bot\u001B[2J:example.org",
otherDeviceId: "PHONE\r123",
transactionId: "txn-\n\u009B31m1",
otherUserId: "@bot\u001B[2J\u009Dspoof\u0007:example.org",
otherDeviceId: "PHONE\r\u009B2J123",
phaseName: "started\u001B[0m",
methods: ["m.sas.v1\nspoof"],
methods: ["m.sas.v1\n\u009B31mspoof"],
chosenMethod: "m.sas.v1\u001B[1m",
sas: {
emoji: [
["🐶", "Dog\u001B[31m"],
["🐱", "Cat\nspoof"],
["🐶", "Dog\u001B[31m\u009B2J"],
["🐱", "Cat\n\u009B31mspoof"],
],
},
error: "Remote\u001B[31m cancelled\nforged",
error: "Remote\u001B[31m cancelled\n\u009B31mforged",
}),
]);
const program = buildProgram();
@@ -450,6 +450,30 @@ describe("matrix CLI verification commands", () => {
expect(consoleLogMock).toHaveBeenCalledWith("Verification error: Remote cancelledforged");
});
it("shell-quotes Matrix verification ids in follow-up command guidance", async () => {
requestMatrixVerificationMock.mockResolvedValue(
mockMatrixVerificationSummary({
id: "self-verify-1",
transactionId: "txn-'$(touch /tmp/pwn)",
}),
);
const program = buildProgram();
await program.parseAsync(["matrix", "verify", "request", "--own-user"], {
from: "user",
});
expect(consoleLogMock).toHaveBeenCalledWith(
"- Then run openclaw matrix verify start 'txn-'\\''$(touch /tmp/pwn)' to start SAS verification.",
);
expect(consoleLogMock).toHaveBeenCalledWith(
"- Run openclaw matrix verify sas 'txn-'\\''$(touch /tmp/pwn)' to display the SAS emoji or decimals.",
);
expect(consoleLogMock).toHaveBeenCalledWith(
"- When the SAS matches, run openclaw matrix verify confirm-sas 'txn-'\\''$(touch /tmp/pwn)'.",
);
});
it("shows Matrix SAS diagnostics and confirm/mismatch guidance", async () => {
getMatrixVerificationSasMock.mockResolvedValue({
decimal: [1234, 5678, 9012],
@@ -464,10 +488,10 @@ describe("matrix CLI verification commands", () => {
});
expect(consoleLogMock).toHaveBeenCalledWith("SAS decimals: 1234 5678 9012");
expect(consoleLogMock).toHaveBeenCalledWith(
"- If they match, run 'openclaw matrix verify confirm-sas self-1'.",
"- If they match, run openclaw matrix verify confirm-sas self-1.",
);
expect(consoleLogMock).toHaveBeenCalledWith(
"- If they do not match, run 'openclaw matrix verify mismatch-sas self-1'.",
"- If they do not match, run openclaw matrix verify mismatch-sas self-1.",
);
});
@@ -483,7 +507,7 @@ describe("matrix CLI verification commands", () => {
await program.parseAsync(["matrix", "verify", "accept", "verification-1"], { from: "user" });
expect(consoleLogMock).toHaveBeenCalledWith(
"- Run 'openclaw matrix verify start txn-stable' to start SAS verification.",
"- Run openclaw matrix verify start txn-stable to start SAS verification.",
);
});
@@ -1261,7 +1285,7 @@ describe("matrix CLI verification commands", () => {
"Backup issue: backup decryption key is not loaded on this device (secret storage did not return a key)",
);
expect(console.log).toHaveBeenCalledWith(
"- Backup key is not loaded on this device. Run 'openclaw matrix verify backup restore' to load it and restore old room keys.",
"- Backup key is not loaded on this device. Run openclaw matrix verify backup restore to load it and restore old room keys.",
);
expect(console.log).not.toHaveBeenCalledWith(
"- Backup is present but not trusted for this device. Re-run 'openclaw matrix verify device <key>'.",
@@ -1328,7 +1352,7 @@ describe("matrix CLI verification commands", () => {
await program.parseAsync(["matrix", "verify", "status"], { from: "user" });
expect(console.log).toHaveBeenCalledWith(
"- If you want a fresh backup baseline and accept losing unrecoverable history, run 'openclaw matrix verify backup reset --yes'. This may also repair secret storage so the new backup key can be loaded after restart.",
"- If you want a fresh backup baseline and accept losing unrecoverable history, run openclaw matrix verify backup reset --yes. This may also repair secret storage so the new backup key can be loaded after restart.",
);
});
@@ -1406,10 +1430,10 @@ describe("matrix CLI verification commands", () => {
});
expect(console.log).toHaveBeenCalledWith("Account: assistant");
expect(console.log).toHaveBeenCalledWith(
"- Run 'openclaw matrix verify device <key> --account assistant' to verify this device.",
"- Run openclaw matrix verify device '<key>' --account assistant to verify this device.",
);
expect(console.log).toHaveBeenCalledWith(
"- Run 'openclaw matrix verify bootstrap --account assistant' to create a room key backup.",
"- Run openclaw matrix verify bootstrap --account assistant to create a room key backup.",
);
});

View File

@@ -120,9 +120,23 @@ function resolveMatrixCliAccountContext(accountId?: string): {
}
function formatMatrixCliCommand(command: string, accountId?: string): string {
return formatMatrixCliCommandParts(command.split(" "), accountId);
}
function formatMatrixCliCommandParts(parts: string[], accountId?: string): string {
const normalizedAccountId = normalizeAccountId(accountId);
const suffix = normalizedAccountId === "default" ? "" : ` --account ${normalizedAccountId}`;
return `openclaw matrix ${command}${suffix}`;
const command = ["openclaw", "matrix", ...parts];
if (normalizedAccountId !== "default") {
command.push("--account", normalizedAccountId);
}
return command.map(formatMatrixCliShellArg).join(" ");
}
function formatMatrixCliShellArg(value: string): string {
if (/^[A-Za-z0-9_./:=@%+-]+$/.test(value)) {
return value;
}
return `'${value.replaceAll("'", "'\\''")}'`;
}
function printMatrixOwnDevices(
@@ -649,6 +663,43 @@ function sanitizeMatrixCliText(value: string): string {
let withoutAnsi = "";
for (let index = 0; index < value.length; index++) {
const code = value.charCodeAt(index);
if (code === 0x9b) {
index++;
while (index < value.length && !isAnsiFinalByte(value.charCodeAt(index))) {
index++;
}
continue;
}
if (code === 0x9d) {
index++;
while (index < value.length) {
const current = value.charCodeAt(index);
if (current === 0x07 || current === 0x9c) {
break;
}
if (current === 0x1b && value[index + 1] === "\\") {
index++;
break;
}
index++;
}
continue;
}
if (code === 0x90 || code === 0x9e || code === 0x9f) {
index++;
while (index < value.length) {
const current = value.charCodeAt(index);
if (current === 0x07 || current === 0x9c) {
break;
}
if (current === 0x1b && value[index + 1] === "\\") {
index++;
break;
}
index++;
}
continue;
}
if (code !== 0x1b) {
withoutAnsi += value[index];
continue;
@@ -683,13 +734,23 @@ function sanitizeMatrixCliText(value: string): string {
let sanitized = "";
for (const character of withoutAnsi) {
const code = character.charCodeAt(0);
if ((code >= 0x20 && code !== 0x7f) || code > 0x7f) {
if (!isUnsafeMatrixCliTerminalCode(code)) {
sanitized += character;
}
}
return sanitized;
}
function isUnsafeMatrixCliTerminalCode(code: number): boolean {
return (
code < 0x20 ||
code === 0x7f ||
(code >= 0x80 && code <= 0x9f) ||
(code >= 0x202a && code <= 0x202e) ||
(code >= 0x2066 && code <= 0x2069)
);
}
function isAnsiFinalByte(code: number): boolean {
return code >= 0x40 && code <= 0x7e;
}
@@ -759,8 +820,8 @@ function printMatrixVerificationSas(sas: MatrixCliVerificationSas): void {
function printMatrixVerificationSasGuidance(requestId: string, accountId?: string): void {
printGuidance([
`Compare the emoji or decimals with the other Matrix client.`,
`If they match, run '${formatMatrixCliCommand(`verify confirm-sas ${requestId}`, accountId)}'.`,
`If they do not match, run '${formatMatrixCliCommand(`verify mismatch-sas ${requestId}`, accountId)}'.`,
`If they match, run ${formatMatrixCliCommandParts(["verify", "confirm-sas", requestId], accountId)}.`,
`If they do not match, run ${formatMatrixCliCommandParts(["verify", "mismatch-sas", requestId], accountId)}.`,
]);
}
@@ -785,9 +846,9 @@ async function promptMatrixVerificationSasMatch(): Promise<boolean> {
function printMatrixVerificationRequestGuidance(requestId: string, accountId?: string): void {
printGuidance([
`Accept the verification request in another Matrix client for this account.`,
`Then run '${formatMatrixCliCommand(`verify start ${requestId}`, accountId)}' to start SAS verification.`,
`Run '${formatMatrixCliCommand(`verify sas ${requestId}`, accountId)}' to display the SAS emoji or decimals.`,
`When the SAS matches, run '${formatMatrixCliCommand(`verify confirm-sas ${requestId}`, accountId)}'.`,
`Then run ${formatMatrixCliCommandParts(["verify", "start", requestId], accountId)} to start SAS verification.`,
`Run ${formatMatrixCliCommandParts(["verify", "sas", requestId], accountId)} to display the SAS emoji or decimals.`,
`When the SAS matches, run ${formatMatrixCliCommandParts(["verify", "confirm-sas", requestId], accountId)}.`,
]);
}
@@ -876,20 +937,20 @@ function buildVerificationGuidance(
if (!status.verified) {
if (status.recoveryKeyAccepted === true && status.backupUsable === true) {
nextSteps.add(
`Recovery key can unlock the room-key backup, but full Matrix identity trust is still incomplete. Run '${formatMatrixCliCommand("verify self", accountId)}' and follow the prompts from another Matrix client.`,
`Recovery key can unlock the room-key backup, but full Matrix identity trust is still incomplete. Run ${formatMatrixCliCommand("verify self", accountId)} and follow the prompts from another Matrix client.`,
);
nextSteps.add(
`If you intend to replace the current cross-signing identity, run '${formatMatrixCliCommand("verify bootstrap --recovery-key <key> --force-reset-cross-signing", accountId)}'.`,
`If you intend to replace the current cross-signing identity, run ${formatMatrixCliCommand("verify bootstrap --recovery-key <key> --force-reset-cross-signing", accountId)}.`,
);
} else {
nextSteps.add(
`Run '${formatMatrixCliCommand("verify device <key>", accountId)}' to verify this device.`,
`Run ${formatMatrixCliCommand("verify device <key>", accountId)} to verify this device.`,
);
}
}
if (backupIssue.code === "missing-server-backup") {
nextSteps.add(
`Run '${formatMatrixCliCommand("verify bootstrap", accountId)}' to create a room key backup.`,
`Run ${formatMatrixCliCommand("verify bootstrap", accountId)} to create a room key backup.`,
);
} else if (
backupIssue.code === "key-load-failed" ||
@@ -898,30 +959,30 @@ function buildVerificationGuidance(
) {
if (status.recoveryKeyStored) {
nextSteps.add(
`Backup key is not loaded on this device. Run '${formatMatrixCliCommand("verify backup restore", accountId)}' to load it and restore old room keys.`,
`Backup key is not loaded on this device. Run ${formatMatrixCliCommand("verify backup restore", accountId)} to load it and restore old room keys.`,
);
} else {
nextSteps.add(
`Store a recovery key with '${formatMatrixCliCommand("verify device <key>", accountId)}', then run '${formatMatrixCliCommand("verify backup restore", accountId)}'.`,
`Store a recovery key with ${formatMatrixCliCommand("verify device <key>", accountId)}, then run ${formatMatrixCliCommand("verify backup restore", accountId)}.`,
);
}
} else if (backupIssue.code === "key-mismatch") {
nextSteps.add(
`Backup key mismatch on this device. Re-run '${formatMatrixCliCommand("verify device <key>", accountId)}' with the matching recovery key.`,
`Backup key mismatch on this device. Re-run ${formatMatrixCliCommand("verify device <key>", accountId)} with the matching recovery key.`,
);
nextSteps.add(
`If you want a fresh backup baseline and accept losing unrecoverable history, run '${formatMatrixCliCommand("verify backup reset --yes", accountId)}'. This may also repair secret storage so the new backup key can be loaded after restart.`,
`If you want a fresh backup baseline and accept losing unrecoverable history, run ${formatMatrixCliCommand("verify backup reset --yes", accountId)}. This may also repair secret storage so the new backup key can be loaded after restart.`,
);
} else if (backupIssue.code === "untrusted-signature") {
nextSteps.add(
`Backup trust chain is not verified on this device. Re-run '${formatMatrixCliCommand("verify device <key>", accountId)}' if you have the correct recovery key.`,
`Backup trust chain is not verified on this device. Re-run ${formatMatrixCliCommand("verify device <key>", accountId)} if you have the correct recovery key.`,
);
nextSteps.add(
`If you want a fresh backup baseline and accept losing unrecoverable history, run '${formatMatrixCliCommand("verify backup reset --yes", accountId)}'. This may also repair secret storage so the new backup key can be loaded after restart.`,
`If you want a fresh backup baseline and accept losing unrecoverable history, run ${formatMatrixCliCommand("verify backup reset --yes", accountId)}. This may also repair secret storage so the new backup key can be loaded after restart.`,
);
} else if (backupIssue.code === "indeterminate") {
nextSteps.add(
`Run '${formatMatrixCliCommand("verify status --verbose", accountId)}' to inspect backup trust diagnostics.`,
`Run ${formatMatrixCliCommand("verify status --verbose", accountId)} to inspect backup trust diagnostics.`,
);
}
if (status.pendingVerifications > 0) {
@@ -1287,7 +1348,7 @@ export function registerMatrixCli(params: { program: Command }): void {
run: async (accountId, cfg) => await acceptMatrixVerification(id, { accountId, cfg }),
afterText: (summary, accountId) => {
printGuidance([
`Run '${formatMatrixCliCommand(`verify start ${formatMatrixVerificationCommandId(summary)}`, accountId)}' to start SAS verification.`,
`Run ${formatMatrixCliCommandParts(["verify", "start", formatMatrixVerificationCommandId(summary)], accountId)} to start SAS verification.`,
]);
},
errorPrefix: "Verification accept failed",

View File

@@ -1,5 +1,6 @@
import { randomUUID } from "node:crypto";
import { mkdir, writeFile } from "node:fs/promises";
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import { setTimeout as sleep } from "node:timers/promises";
import type { MatrixVerificationSummary } from "@openclaw/matrix/test-api.js";
@@ -304,8 +305,8 @@ async function writeMatrixQaCliOutputArtifacts(params: {
const stdoutPath = path.join(params.rootDir, `${prefix}.stdout.txt`);
const stderrPath = path.join(params.rootDir, `${prefix}.stderr.txt`);
await Promise.all([
writeFile(stdoutPath, params.result.stdout),
writeFile(stderrPath, params.result.stderr),
writeFile(stdoutPath, params.result.stdout, { mode: 0o600 }),
writeFile(stderrPath, params.result.stderr, { mode: 0o600 }),
]);
return { stderrPath, stdoutPath };
}
@@ -376,13 +377,15 @@ async function createMatrixQaCliSelfVerificationRuntime(params: {
userId: string;
}) {
const outputDir = requireMatrixQaE2eeOutputDir(params.context);
const rootDir = path.join(
const rootDir = await mkdtemp(path.join(tmpdir(), "openclaw-matrix-cli-qa-"));
const artifactDir = path.join(
outputDir,
"cli-self-verification",
randomUUID().replaceAll("-", "").slice(0, 12),
);
const stateDir = path.join(rootDir, "state");
const configPath = path.join(rootDir, "config.json");
await mkdir(artifactDir, { recursive: true });
await mkdir(stateDir, { recursive: true });
await writeFile(
configPath,
@@ -436,8 +439,11 @@ async function createMatrixQaCliSelfVerificationRuntime(params: {
});
return {
configPath,
dispose: async () => {
await rm(rootDir, { force: true, recursive: true });
},
run,
rootDir,
rootDir: artifactDir,
start,
stateDir,
};
@@ -1175,181 +1181,187 @@ export async function runMatrixQaE2eeCliSelfVerificationScenario(
deviceId: cliDevice.deviceId,
userId: cliDevice.userId,
});
const restoreResult = await cli.run([
"matrix",
"verify",
"backup",
"restore",
"--account",
accountId,
"--recovery-key",
encodedRecoveryKey,
"--json",
]);
const restoreArtifacts = await writeMatrixQaCliOutputArtifacts({
label: "verify-backup-restore",
result: restoreResult,
rootDir: cli.rootDir,
});
const restored = parseMatrixQaCliJson(restoreResult) as MatrixQaCliBackupRestoreStatus;
if (
restored.success !== true ||
restored.backup?.decryptionKeyCached !== true ||
restored.backup?.matchesDecryptionKey !== true ||
restored.backup?.keyLoadError
) {
throw new Error(
`Matrix CLI recovery key did not load matching room-key backup material before self-verification: ${
restored.error ?? restored.backup?.keyLoadError ?? "unknown backup state"
}`,
);
}
const session = cli.start(["matrix", "verify", "self", "--account", accountId]);
try {
const requestOutput = await session.waitForOutput(
(output) => output.text.includes("Accept this verification request"),
"self-verification request guidance",
context.timeoutMs,
);
const cliTransactionId = parseMatrixQaCliSummaryField(requestOutput.text, "Transaction id");
const ownerRequested = await waitForMatrixQaVerificationSummary({
client: owner,
label: "owner received CLI self-verification request",
predicate: (summary) =>
isMatrixQaCliOwnerSelfVerification({
cliDeviceId: cliTransactionId ? undefined : cliDevice.deviceId,
driverUserId: context.driverUserId,
requirePending: true,
summary,
transactionId: cliTransactionId ?? undefined,
}),
timeoutMs: context.timeoutMs,
});
if (ownerRequested.canAccept) {
await owner.acceptVerification(ownerRequested.id);
}
const sasOutput = await session.waitForOutput(
(output) => /^SAS (?:emoji|decimals):/m.test(output.text),
"SAS emoji or decimals",
context.timeoutMs,
);
const cliSas = parseMatrixQaCliSasText(
sasOutput.text,
"interactive openclaw matrix verify self",
);
const ownerSas = await waitForMatrixQaVerificationSummary({
client: owner,
label: "owner SAS for CLI self-verification",
predicate: (summary) =>
isMatrixQaCliOwnerSelfVerification({
cliDeviceId: cliTransactionId ? undefined : cliDevice.deviceId,
driverUserId: context.driverUserId,
requireSas: true,
summary,
transactionId: cliTransactionId ?? undefined,
}),
timeoutMs: context.timeoutMs,
});
const sasArtifact = assertMatrixQaCliSasMatches({
cliSas,
owner: ownerSas,
});
await session.writeStdin("yes\n");
await owner.confirmVerificationSas(ownerSas.id);
const completedCli = await session.wait();
const selfVerificationArtifacts = await writeMatrixQaCliOutputArtifacts({
label: "verify-self",
result: completedCli,
rootDir: cli.rootDir,
});
if (!/^Device verified by owner:\s*yes$/m.test(completedCli.stdout)) {
throw new Error(
"Interactive Matrix CLI self-verification did not report final device verification",
);
}
if (!/^Cross-signing verified:\s*yes$/m.test(completedCli.stdout)) {
throw new Error(
"Interactive Matrix CLI self-verification did not report full Matrix identity trust",
);
}
const completedOwner = await waitForMatrixQaVerificationSummary({
client: owner,
label: "owner completed CLI self-verification",
predicate: (summary) =>
isMatrixQaCliOwnerSelfVerification({
cliDeviceId: cliTransactionId ? undefined : cliDevice.deviceId,
driverUserId: context.driverUserId,
requireCompleted: true,
summary,
transactionId: cliTransactionId ?? undefined,
}),
timeoutMs: context.timeoutMs,
});
const cliVerificationId =
completedCli.stdout.match(/^Verification id:\s*(\S+)/m)?.[1] ?? "interactive-cli";
const statusResult = await cli.run([
const restoreResult = await cli.run([
"matrix",
"verify",
"status",
"backup",
"restore",
"--account",
accountId,
"--recovery-key",
encodedRecoveryKey,
"--json",
]);
const statusArtifacts = await writeMatrixQaCliOutputArtifacts({
label: "verify-status",
result: statusResult,
const restoreArtifacts = await writeMatrixQaCliOutputArtifacts({
label: "verify-backup-restore",
result: restoreResult,
rootDir: cli.rootDir,
});
const status = parseMatrixQaCliJson(statusResult) as MatrixQaCliVerificationStatus;
const restored = parseMatrixQaCliJson(restoreResult) as MatrixQaCliBackupRestoreStatus;
if (
status.verified !== true ||
status.crossSigningVerified !== true ||
status.signedByOwner !== true ||
status.backup?.trusted !== true ||
status.backup?.matchesDecryptionKey !== true ||
status.backup?.keyLoadError
restored.success !== true ||
restored.backup?.decryptionKeyCached !== true ||
restored.backup?.matchesDecryptionKey !== true ||
restored.backup?.keyLoadError
) {
throw new Error(
`Matrix CLI device was not fully usable after SAS completion: ownerVerified=${
status.verified === true &&
status.crossSigningVerified === true &&
status.signedByOwner === true
? "yes"
: "no"
}, backupUsable=${isMatrixQaCliBackupUsable(status.backup) ? "yes" : "no"}${
status.backup?.keyLoadError ? `, backupError=${status.backup.keyLoadError}` : ""
`Matrix CLI recovery key did not load matching room-key backup material before self-verification: ${
restored.error ?? restored.backup?.keyLoadError ?? "unknown backup state"
}`,
);
}
return {
artifacts: {
completedVerificationIds: [cliVerificationId, completedOwner.id],
currentDeviceId: status.deviceId ?? cliDevice.deviceId,
...(cliSas.kind === "emoji" ? { sasEmoji: sasArtifact } : {}),
secondaryDeviceId: cliDevice.deviceId,
},
details: [
"Matrix CLI self-verification established full Matrix identity trust through interactive openclaw matrix verify self",
`cli config path: ${cli.configPath}`,
`cli state dir: ${cli.stateDir}`,
`cli backup restore stdout: ${restoreArtifacts.stdoutPath}`,
`cli backup restore stderr: ${restoreArtifacts.stderrPath}`,
`cli verify self stdout: ${selfVerificationArtifacts.stdoutPath}`,
`cli verify self stderr: ${selfVerificationArtifacts.stderrPath}`,
`cli verify status stdout: ${statusArtifacts.stdoutPath}`,
`cli verify status stderr: ${statusArtifacts.stderrPath}`,
`cli device: ${cliDevice.deviceId}`,
`cli verification id: ${cliVerificationId}`,
`owner-side verification id: ${completedOwner.id}`,
`transaction: ${completedOwner.transactionId ?? "<none>"}`,
`cli verified by owner: ${status.verified ? "yes" : "no"}`,
`cli cross-signing verified: ${status.crossSigningVerified ? "yes" : "no"}`,
`cli backup usable: ${isMatrixQaCliBackupUsable(status.backup) ? "yes" : "no"}`,
].join("\n"),
};
const session = cli.start(["matrix", "verify", "self", "--account", accountId]);
try {
const requestOutput = await session.waitForOutput(
(output) => output.text.includes("Accept this verification request"),
"self-verification request guidance",
context.timeoutMs,
);
const cliTransactionId = parseMatrixQaCliSummaryField(
requestOutput.text,
"Transaction id",
);
const ownerRequested = await waitForMatrixQaVerificationSummary({
client: owner,
label: "owner received CLI self-verification request",
predicate: (summary) =>
isMatrixQaCliOwnerSelfVerification({
cliDeviceId: cliTransactionId ? undefined : cliDevice.deviceId,
driverUserId: context.driverUserId,
requirePending: true,
summary,
transactionId: cliTransactionId ?? undefined,
}),
timeoutMs: context.timeoutMs,
});
if (ownerRequested.canAccept) {
await owner.acceptVerification(ownerRequested.id);
}
const sasOutput = await session.waitForOutput(
(output) => /^SAS (?:emoji|decimals):/m.test(output.text),
"SAS emoji or decimals",
context.timeoutMs,
);
const cliSas = parseMatrixQaCliSasText(
sasOutput.text,
"interactive openclaw matrix verify self",
);
const ownerSas = await waitForMatrixQaVerificationSummary({
client: owner,
label: "owner SAS for CLI self-verification",
predicate: (summary) =>
isMatrixQaCliOwnerSelfVerification({
cliDeviceId: cliTransactionId ? undefined : cliDevice.deviceId,
driverUserId: context.driverUserId,
requireSas: true,
summary,
transactionId: cliTransactionId ?? undefined,
}),
timeoutMs: context.timeoutMs,
});
const sasArtifact = assertMatrixQaCliSasMatches({
cliSas,
owner: ownerSas,
});
await session.writeStdin("yes\n");
await owner.confirmVerificationSas(ownerSas.id);
const completedCli = await session.wait();
const selfVerificationArtifacts = await writeMatrixQaCliOutputArtifacts({
label: "verify-self",
result: completedCli,
rootDir: cli.rootDir,
});
if (!/^Device verified by owner:\s*yes$/m.test(completedCli.stdout)) {
throw new Error(
"Interactive Matrix CLI self-verification did not report final device verification",
);
}
if (!/^Cross-signing verified:\s*yes$/m.test(completedCli.stdout)) {
throw new Error(
"Interactive Matrix CLI self-verification did not report full Matrix identity trust",
);
}
const completedOwner = await waitForMatrixQaVerificationSummary({
client: owner,
label: "owner completed CLI self-verification",
predicate: (summary) =>
isMatrixQaCliOwnerSelfVerification({
cliDeviceId: cliTransactionId ? undefined : cliDevice.deviceId,
driverUserId: context.driverUserId,
requireCompleted: true,
summary,
transactionId: cliTransactionId ?? undefined,
}),
timeoutMs: context.timeoutMs,
});
const cliVerificationId =
completedCli.stdout.match(/^Verification id:\s*(\S+)/m)?.[1] ?? "interactive-cli";
const statusResult = await cli.run([
"matrix",
"verify",
"status",
"--account",
accountId,
"--json",
]);
const statusArtifacts = await writeMatrixQaCliOutputArtifacts({
label: "verify-status",
result: statusResult,
rootDir: cli.rootDir,
});
const status = parseMatrixQaCliJson(statusResult) as MatrixQaCliVerificationStatus;
if (
status.verified !== true ||
status.crossSigningVerified !== true ||
status.signedByOwner !== true ||
status.backup?.trusted !== true ||
status.backup?.matchesDecryptionKey !== true ||
status.backup?.keyLoadError
) {
throw new Error(
`Matrix CLI device was not fully usable after SAS completion: ownerVerified=${
status.verified === true &&
status.crossSigningVerified === true &&
status.signedByOwner === true
? "yes"
: "no"
}, backupUsable=${isMatrixQaCliBackupUsable(status.backup) ? "yes" : "no"}${
status.backup?.keyLoadError ? `, backupError=${status.backup.keyLoadError}` : ""
}`,
);
}
return {
artifacts: {
completedVerificationIds: [cliVerificationId, completedOwner.id],
currentDeviceId: status.deviceId ?? cliDevice.deviceId,
...(cliSas.kind === "emoji" ? { sasEmoji: sasArtifact } : {}),
secondaryDeviceId: cliDevice.deviceId,
},
details: [
"Matrix CLI self-verification established full Matrix identity trust through interactive openclaw matrix verify self",
"cli secret config cleaned after run: yes",
`cli backup restore stdout: ${restoreArtifacts.stdoutPath}`,
`cli backup restore stderr: ${restoreArtifacts.stderrPath}`,
`cli verify self stdout: ${selfVerificationArtifacts.stdoutPath}`,
`cli verify self stderr: ${selfVerificationArtifacts.stderrPath}`,
`cli verify status stdout: ${statusArtifacts.stdoutPath}`,
`cli verify status stderr: ${statusArtifacts.stderrPath}`,
`cli device: ${cliDevice.deviceId}`,
`cli verification id: ${cliVerificationId}`,
`owner-side verification id: ${completedOwner.id}`,
`transaction: ${completedOwner.transactionId ?? "<none>"}`,
`cli verified by owner: ${status.verified ? "yes" : "no"}`,
`cli cross-signing verified: ${status.crossSigningVerified ? "yes" : "no"}`,
`cli backup usable: ${isMatrixQaCliBackupUsable(status.backup) ? "yes" : "no"}`,
].join("\n"),
};
} finally {
session.kill();
}
} finally {
session.kill();
await cli.dispose();
}
},
);

View File

@@ -3013,7 +3013,20 @@ describe("matrix live qa scenarios", () => {
waitForOutput,
writeStdin,
});
runMatrixQaOpenClawCli.mockImplementation(async ({ args }) => {
let cliAccountConfigDuringRun: Record<string, unknown> | null = null;
runMatrixQaOpenClawCli.mockImplementation(async ({ args, env }) => {
if (!cliAccountConfigDuringRun && env.OPENCLAW_CONFIG_PATH) {
const cliConfig = JSON.parse(
await readFile(String(env.OPENCLAW_CONFIG_PATH), "utf8"),
) as {
channels?: {
matrix?: {
accounts?: Record<string, Record<string, unknown>>;
};
};
};
cliAccountConfigDuringRun = cliConfig.channels?.matrix?.accounts?.cli ?? null;
}
const joined = args.join(" ");
if (joined === "matrix verify status --account cli --json") {
return {
@@ -3111,17 +3124,10 @@ describe("matrix live qa scenarios", () => {
["matrix", "verify", "status", "--account", "cli", "--json"],
]);
const cliEnv = startMatrixQaOpenClawCli.mock.calls[0]?.[0].env;
expect(cliEnv?.OPENCLAW_STATE_DIR).toContain("cli-self-verification");
expect(cliEnv?.OPENCLAW_CONFIG_PATH).toContain("cli-self-verification");
expect(cliEnv?.OPENCLAW_STATE_DIR).toContain("openclaw-matrix-cli-qa-");
expect(cliEnv?.OPENCLAW_CONFIG_PATH).toContain("openclaw-matrix-cli-qa-");
const configPath = String(cliEnv?.OPENCLAW_CONFIG_PATH);
const cliConfig = JSON.parse(await readFile(configPath, "utf8")) as {
channels?: {
matrix?: {
accounts?: Record<string, Record<string, unknown>>;
};
};
};
expect(cliConfig.channels?.matrix?.accounts?.cli).toMatchObject({
expect(cliAccountConfigDuringRun).toMatchObject({
accessToken: "cli-token",
deviceId: "CLIDEVICE",
encryption: true,
@@ -3129,6 +3135,8 @@ describe("matrix live qa scenarios", () => {
startupVerification: "off",
userId: "@driver:matrix-qa.test",
});
await expect(readFile(configPath, "utf8")).rejects.toThrow();
await expect(readdir(String(cliEnv?.OPENCLAW_STATE_DIR))).rejects.toThrow();
expect(acceptVerification).toHaveBeenCalledWith("owner-request");
expect(confirmVerificationSas).toHaveBeenCalledWith("owner-request");
expect(deleteOwnDevices).not.toHaveBeenCalled();