mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 12:40:43 +00:00
fix: harden Matrix CLI verification artifacts
This commit is contained in:
@@ -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.",
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user