improve(qa-matrix): split E2EE runtime and protect recovery keys (#107828)

* fix(autoreview): handle verified QA source references

* refactor(qa-matrix): split E2EE scenario runtime

* style(qa-matrix): format recovery scenario

* chore(qa-matrix): prune split runtime baseline

* chore: keep release note in PR
This commit is contained in:
Peter Steinberger
2026-07-14 16:13:41 -07:00
committed by GitHub
parent e4e83cf813
commit 623ec0f369
21 changed files with 4864 additions and 3731 deletions

View File

@@ -278,11 +278,16 @@ class ReviewChunk(NamedTuple):
context: str = ""
SECRET_PLACEHOLDER_VALUES = {
"changeme",
"decoy-token",
"dummy",
"example",
"fake",
"gateway-token",
"matrix_qa_e2ee_cli_gateway",
"matrix_qa_e2ee_thread",
"matrix_qa_e2ee_verify_notice",
"not-a-real",
"not-a-valid-matrix-recovery-key",
"placeholder",
"redacted",
"sample",
@@ -455,6 +460,27 @@ BACKTICK_TEMPLATE_INTERPOLATION_PATTERN = re.compile(r"\$\{([^{}\r\n]+)\}")
BACKTICK_TEMPLATE_SAFE_LITERAL_PATTERN = re.compile(
r"(?i)(?:(?:Bearer|Basic)[ \t]+|[ \t:./,_-]*)"
)
BACKTICK_TEMPLATE_FIXTURE_PREFIX_VALUES = {"matrix-qa-"}
SOURCE_CODE_REFERENCE_VALUES = {
"cliDevice.accessToken",
"context.driverAccessToken",
"context.driverPassword",
"context.observerAccessToken",
"context.observerPassword",
"context.sutAccessToken",
"context.sutPassword",
"driverAccount.accessToken",
"driverAccount.password",
"driverPassword",
"recoveryDevice.accessToken",
"recoveryDevice.password",
}
SOURCE_CODE_REFERENCE_PATTERN = re.compile(
r"(?<![A-Za-z0-9_$?.])(?:" + "|".join(
re.escape(value)
for value in sorted(SOURCE_CODE_REFERENCE_VALUES, key=len, reverse=True)
) + r")(?![A-Za-z0-9_$])"
)
DEFAULT_ENGINE_PATHS = ("/usr/local/bin", "/usr/bin", "/bin")
# Keep this explicit: suffix matching leaks unrelated process credentials such
# as package-registry and telemetry tokens into reviewer subprocesses.
@@ -1906,8 +1932,19 @@ def raw_repo_path_has_symlink_component(repo: Path, rel_path: Path) -> bool:
return False
def fallback_expression(text: str) -> str:
operator = re.match(r"\s*(?:\|\||&&|\?\?|\+|\?|or\b|and\b|if\b|unless\b)", text)
def fallback_expression(text: str, *, typescript: bool = False) -> str:
operator_keywords = (
r"|as(?![\w$])|satisfies(?![\w$])" if typescript else ""
)
operator = re.match(
r"\s*(?:\|\||&&|\?\?|[=<>!~:+\-*/%&|^?.,]"
r"|and\b|else\b|if\b|in(?![\w$])|instanceof(?![\w$])"
r"|is\b|not\b|or\b"
r"|unless\b"
+ operator_keywords
+ r")",
text,
)
cursor = operator.end() if operator is not None else 0
stack: list[str] = []
operand_started = False
@@ -1920,9 +1957,16 @@ def fallback_expression(text: str) -> str:
char = text[cursor]
next_char = text[cursor + 1] if cursor + 1 < len(text) else ""
if line_comment:
if char == "\n":
if char in "\r\n\u2028\u2029":
line_comment = False
if operand_started and not stack:
if (
operand_started
and not stack
and not javascript_continues_after_line_terminator(
text[cursor + 1 :],
typescript=typescript,
)
):
break
cursor += 1
continue
@@ -1974,14 +2018,54 @@ def fallback_expression(text: str) -> str:
continue
if not stack and char in ",;)]}":
break
if char == "\n" and operand_started and not stack:
break
if not char.isspace():
if char in "\r\n\u2028\u2029" and operand_started and not stack:
if not javascript_continues_after_line_terminator(
text[cursor + 1 :],
typescript=typescript,
):
break
if not stack and (char.isalpha() or char in "_$"):
word_end = cursor + 1
while word_end < len(text) and (
text[word_end].isalnum() or text[word_end] in "_$"
):
word_end += 1
word = text[cursor:word_end]
word_operators = {"in", "instanceof"}
if typescript:
word_operators.update({"as", "satisfies"})
operand_started = word not in word_operators
cursor = word_end
continue
if not stack and char in "=<>!~:+-*/%&|^?.":
operand_started = False
elif not char.isspace():
operand_started = True
cursor += 1
return text[:cursor]
def javascript_continues_after_line_terminator(
text: str,
*,
typescript: bool = False,
) -> bool:
suffix = text.lstrip()
if suffix.startswith(("++", "--", "~")) or (
suffix.startswith("!") and not suffix.startswith("!=")
):
return False
type_operators = (
r"|as(?![\w$])|satisfies(?![\w$])" if typescript else ""
)
return re.match(
r"(?:[=<>!:+\-*/%&|^?.,([`]|in(?![\w$])|instanceof(?![\w$])"
+ type_operators
+ r")",
suffix,
) is not None
def top_level_fallback_suffix(
text: str,
*,
@@ -3717,9 +3801,14 @@ def secret_literal_risk(expression: str, minimum_length: int = 12) -> bool:
return False
def fallback_secret_risk(text: str, minimum_length: int = 8) -> bool:
def fallback_secret_risk(
text: str,
minimum_length: int = 8,
*,
typescript: bool = False,
) -> bool:
return secret_literal_risk(
fallback_expression(text),
fallback_expression(text, typescript=typescript),
minimum_length=minimum_length,
)
@@ -4610,27 +4699,37 @@ def safe_secret_call_suffix(text: str, end: int, call_target: str) -> bool:
return safe_secret_assignment_suffix(text, cursor)
def safe_backtick_literal_segment(value: str) -> bool:
return bool(
BACKTICK_TEMPLATE_SAFE_LITERAL_PATTERN.fullmatch(value)
or value.casefold() in BACKTICK_TEMPLATE_FIXTURE_PREFIX_VALUES
)
def safe_backtick_interpolation_expression(expression: str) -> bool:
quoted_reference = "${" + expression + "}"
if any(
pattern.fullmatch(quoted_reference)
for pattern in QUOTED_SECRET_REFERENCE_PATTERNS
):
return True
return re.fullmatch(r"(?:crypto\.)?randomUUID\(\)", expression) is not None
def safe_backtick_secret_template(value: str) -> bool:
cursor = 0
found_interpolation = False
for match in BACKTICK_TEMPLATE_INTERPOLATION_PATTERN.finditer(value):
literal = value[cursor : match.start()]
if BACKTICK_TEMPLATE_SAFE_LITERAL_PATTERN.fullmatch(literal) is None:
if not safe_backtick_literal_segment(literal):
return False
expression = match.group(1).strip()
quoted_reference = "${" + expression + "}"
if not any(
pattern.fullmatch(quoted_reference)
for pattern in QUOTED_SECRET_REFERENCE_PATTERNS
):
if not safe_backtick_interpolation_expression(expression):
return False
found_interpolation = True
cursor = match.end()
literal = value[cursor:]
return (
found_interpolation
and BACKTICK_TEMPLATE_SAFE_LITERAL_PATTERN.fullmatch(literal) is not None
)
return found_interpolation and safe_backtick_literal_segment(literal)
def javascript_template_literal_end(
@@ -4764,6 +4863,82 @@ def mask_reference_declaration_evidence(text: str) -> str:
return mask_csharp_evidence_prefix("".join(masked))
@functools.lru_cache(maxsize=8)
def javascript_reference_spans(text: str) -> frozenset[tuple[int, int]]:
return frozenset(
(match.start(), match.end())
for match in SOURCE_CODE_REFERENCE_PATTERN.finditer(text)
)
def javascript_source_whitespace(char: str) -> bool:
return (
char in "\t\v\f \r\n\u2028\u2029\ufeff"
or unicodedata.category(char) == "Zs"
)
def safe_javascript_reference_suffix(
text: str,
end: int,
*,
typescript: bool,
) -> bool:
line_terminators = "\r\n\u2028\u2029"
cursor = end
saw_newline = False
while cursor < len(text):
while cursor < len(text) and javascript_source_whitespace(text[cursor]):
saw_newline = saw_newline or text[cursor] in line_terminators
cursor += 1
if text.startswith("//", cursor):
newline = re.search(r"[\r\n\u2028\u2029]", text[cursor + 2 :])
if newline is None:
return True
cursor += 2 + newline.start()
saw_newline = True
continue
if text.startswith("/*", cursor):
comment_end = text.find("*/", cursor + 2)
if comment_end < 0:
return True
comment = text[cursor : comment_end + 2]
saw_newline = saw_newline or any(
terminator in comment for terminator in line_terminators
)
cursor = comment_end + 2
continue
break
if cursor >= len(text):
return True
if text[cursor] in ";}])" or text[cursor] == ",":
return True
if saw_newline and (
text.startswith(("++", "--"), cursor)
or text[cursor] == "~"
or (text[cursor] == "!" and not text.startswith("!=", cursor))
):
return True
continuation_keyword = re.match(
(
r"(?:as|in|instanceof|satisfies)(?![\w$])"
if typescript
else r"(?:in|instanceof)(?![\w$])"
),
text[cursor:],
)
continuation = (
text[cursor] in "=<>!~:+-*/%&|^?.,([`"
or continuation_keyword is not None
)
if continuation:
return not fallback_secret_risk(
text[cursor:],
typescript=typescript,
)
return saw_newline
def bare_code_reference(
text: str,
start: int,
@@ -4848,7 +5023,11 @@ def basic_authorization_risk(text: str) -> bool:
return False
def secret_text_risk(text: str) -> bool:
def secret_text_risk(
text: str,
*,
javascript_dialect: str | None = None,
) -> bool:
uri_authorities = uri_authority_ranges(text)
if credentialed_uri_risk(text, uri_authorities) or basic_authorization_risk(text) or any(
pattern.search(text) for pattern in SECRET_VALUE_PATTERNS
@@ -4869,6 +5048,11 @@ def secret_text_risk(text: str) -> bool:
text,
{prefix.start() for prefix in assignment_prefixes},
)
source_reference_spans = (
javascript_reference_spans(text)
if javascript_dialect is not None
else frozenset()
)
for prefix in assignment_prefixes:
fallback = top_level_fallback_suffix(
text[prefix.end() :],
@@ -4945,6 +5129,18 @@ def secret_text_risk(text: str) -> bool:
and safe_secret_assignment_suffix(text, match.end())
):
continue
if not quoted:
source_start = match.end() - len(value)
source_end = match.end() - (1 if value.endswith("!") else 0)
if (
(source_start, source_end) in source_reference_spans
and safe_javascript_reference_suffix(
text,
match.end(),
typescript=javascript_dialect == "typescript",
)
):
continue
reference_patterns = (
QUOTED_SECRET_REFERENCE_PATTERNS
if quoted
@@ -4991,8 +5187,13 @@ def secret_text_risk(text: str) -> bool:
return False
def require_no_secret_values(label: str, text: str) -> None:
if secret_text_risk(text):
def require_no_secret_values(
label: str,
text: str,
*,
javascript_dialect: str | None = None,
) -> None:
if secret_text_risk(text, javascript_dialect=javascript_dialect):
raise SystemExit(
"refusing to include secret-like content in review bundle; "
f"clean or redact {label} before running autoreview"
@@ -5177,6 +5378,82 @@ def tracked_sensitive_repo_path_risk(rel: str) -> str | None:
return None
def javascript_review_dialect(rel: str) -> str | None:
suffix = Path(rel).suffix.lower()
if suffix in {".cts", ".mts", ".ts", ".tsx"}:
return "typescript"
if suffix in {".cjs", ".js", ".jsx", ".mjs"}:
return "javascript"
return None
def git_c_unquote(value: str) -> str | None:
if len(value) < 2 or value[0] != '"' or value[-1] != '"':
return None
escapes = {
"a": 7,
"b": 8,
"f": 12,
"n": 10,
"r": 13,
"t": 9,
"v": 11,
"\\": 92,
'"': 34,
}
decoded = bytearray()
cursor = 1
while cursor < len(value) - 1:
char = value[cursor]
if char != "\\":
decoded.extend(char.encode("utf-8"))
cursor += 1
continue
cursor += 1
if cursor >= len(value) - 1:
return None
escape = value[cursor]
if escape in escapes:
decoded.append(escapes[escape])
cursor += 1
continue
octal = re.match(r"[0-7]{1,3}", value[cursor:-1])
if octal is None:
return None
decoded.append(int(octal.group(0), 8))
cursor += octal.end()
try:
return decoded.decode("utf-8")
except UnicodeDecodeError:
return None
def diff_marker_path(value: str) -> str | None:
if value == "/dev/null":
return None
if value.startswith('"'):
decoded = git_c_unquote(value)
if decoded is None:
return None
value = decoded
if not value.startswith(("a/", "b/")):
return None
return value[2:]
def diff_section_paths(section: str) -> tuple[str | None, str | None]:
old_path: str | None = None
new_path: str | None = None
for line in section.splitlines():
if line.startswith("@@"):
break
if line.startswith("--- "):
old_path = diff_marker_path(line[4:])
elif line.startswith("+++ "):
new_path = diff_marker_path(line[4:])
return old_path, new_path
def validate_review_patch(
label: str,
paths: list[str],
@@ -5202,8 +5479,33 @@ def validate_review_patch(
f"({patch_bytes} bytes; limit {limit}); split the change into smaller review targets"
)
require_no_secret_values(label, unified_diff_metadata(patch))
for content in unified_diff_contents(patch):
require_no_secret_values(label, content)
sections = [
unit
for unit in review_bundle_units(patch)
if unit.startswith("diff --git ")
]
if sections:
expected_paths = set(paths)
for section in sections:
old_path, new_path = diff_section_paths(section)
old_content, new_content = unified_diff_contents(section)
for rel, content in (
(old_path, old_content),
(new_path, new_content),
):
javascript_dialect = (
javascript_review_dialect(rel)
if rel is not None and rel in expected_paths
else None
)
require_no_secret_values(
f"{label} {rel or '<unknown>'}",
content,
javascript_dialect=javascript_dialect,
)
else:
for content in unified_diff_contents(patch):
require_no_secret_values(label, content)
return patch
@@ -5312,7 +5614,10 @@ def file_bundle_snapshot(
text = data.decode("utf-8")
except UnicodeDecodeError:
return "", True, "non-UTF-8 file"
if secret_text_risk(text):
if secret_text_risk(
text,
javascript_dialect=javascript_review_dialect(rel),
):
return "", True, "secret-like content"
return text, False, None

View File

@@ -1859,6 +1859,562 @@ class AutoreviewHardeningTests(unittest.TestCase):
)
)
def test_secret_detector_allows_typescript_object_secret_references(self) -> None:
content = (
"async function configure(context: RuntimeContext) {\n"
" const cliDevice = await login();\n"
" const driverPassword = readPassword();\n"
" return {\n"
" access"
+ "Token"
+ ": cliDevice.access"
+ "Token,\n"
+ " pass"
+ "word: driverPass"
+ "word,\n"
+ " ...(context.driverPass"
+ "word ? { pass"
+ "word: context.driverPass"
+ "word } : {}),\n"
+ " };\n"
+ "}"
)
yaml_literal = "pass" + "word: actualProductionSecret,"
yaml_reference = "pass" + "word: context.driverPass" + "word"
yaml_flow_reference = (
"{ pass" + "word: context.driverPass" + "word, enabled: true }"
)
sut_reference = (
"const pass"
+ "word = context.sutPass"
+ "word;"
)
literal_value = "actual-production-" + "secret"
source_literal = (
"function configure() { return { pass"
+ 'word: "'
+ literal_value
+ '" }; }'
)
jwt_like = "eyJhbGciOiJIUzI1NiJ9" + ".payload." + "signature"
undeclared_member = (
"const config = { pass"
+ "word: "
+ jwt_like
+ " };"
)
jwt_root = jwt_like.split(".", 1)[0]
declared_member = (
f"const {jwt_root} = {{}};\n"
+ "const config = { pass"
+ "word: "
+ jwt_like
+ " };"
)
prefixed_member = (
"const session = {};\n"
+ "const config = { pass"
+ "word: session."
+ jwt_like
+ " };"
)
declared_identifier = "CorrectHorseBattery" + "Staple123"
declared_identifier_value = (
f"const {declared_identifier} = 0;\n"
+ "const config = { pass"
+ "word: "
+ declared_identifier
+ " };"
)
suffixed_reference = (
"const config = { pass"
+ "word: context.driverPass"
+ "wordExtra };"
)
prefixed_reference = (
"const config = { pass"
+ "word: xcontext.driverPass"
+ "word };"
)
final_property = (
"function configure(context: RuntimeContext) { return { pass"
+ "word: context.driverPass"
+ "word }; }"
)
inline_property = (
"function configure(context: RuntimeContext) { return { pass"
+ "word: context.driverPass"
+ "word, enabled: true }; }"
)
asserted_property = (
"function configure(context: RuntimeContext) { return { pass"
+ "word: context.driverPass"
+ "word! }; }"
)
cast_property = (
"function configure(context: RuntimeContext) { return { pass"
+ "word: context.driverPass"
+ "word as string }; }"
)
union_cast_property = (
"function configure(context: RuntimeContext) { return { pass"
+ "word: context.driverPass"
+ "word as string | undefined }; }"
)
next_statement = (
"function configure(context: RuntimeContext) {\n"
+ " const pass"
+ "word = context.driverPass"
+ "word\n"
+ " return consume();\n"
+ "}"
)
line_comment = (
"const pass"
+ "word = context.driverPass"
+ "word // supplied by CI\n"
+ "consume();"
)
block_comment = (
"const pass"
+ "word = context.driverPass"
+ "word /* supplied by CI */;"
)
concatenated_literal = (
"function configure(context: RuntimeContext) { return { pass"
+ "word: context.driverPass"
+ 'word + "'
+ literal_value
+ '" }; }'
)
continued_literal = (
"function configure(context: RuntimeContext) { return { pass"
+ "word: context.driverPass"
+ "word\n"
+ ' + "'
+ literal_value
+ '" }; }'
)
fallback_literal = (
"function configure(context: RuntimeContext) { return { pass"
+ "word: context.driverPass"
+ 'word ?? "'
+ literal_value
+ '" }; }'
)
assigned_literal = (
"const pass"
+ "word = context.driverPass"
+ "word\n"
+ ' = "'
+ literal_value
+ '";'
)
newline_cast_literal = (
"const pass"
+ "word = context.driverPass"
+ "word\n"
+ " as string + \""
+ literal_value
+ '";'
)
commented_continuation = (
"const pass"
+ "word = context.driverPass"
+ "word // supplied by CI\n"
+ ' + "'
+ literal_value
+ '";'
)
unicode_comment_continuation = (
"const pass"
+ "word = context.driverPass"
+ "word // supplied by CI"
+ chr(0x2028)
+ ' + "'
+ literal_value
+ '";'
)
unicode_space_continuation = (
"const pass"
+ "word = context.driverPass"
+ "word\n"
+ chr(0x00A0)
+ '+ "'
+ literal_value
+ '";'
)
multiline_cast_literal = (
"const pass"
+ "word = context.driverPass"
+ "word as string\n"
+ '+ "'
+ literal_value
+ '";'
)
leading_comma_statement = (
"const pass"
+ "word = context.driverPass"
+ "word\n"
+ ', username = "'
+ literal_value
+ '";'
)
unary_statement = (
"const pass"
+ "word = context.driverPass"
+ "word\n"
+ '!audit("'
+ literal_value
+ '");'
)
inequality_literal = (
"const pass"
+ "word = context.driverPass"
+ "word\n"
+ '!== "'
+ literal_value
+ '";'
)
member_call_literal = (
"const pass"
+ "word = context.driverPass"
+ 'word["concat"]("safe", "'
+ literal_value
+ '");'
)
continued_then_unary_statement = (
"const pass"
+ "word = context.driverPass"
+ "word + suffix\n"
+ '!audit("'
+ literal_value
+ '");'
)
trailing_operator_literal = (
"const pass"
+ "word = context.driverPass"
+ "word + suffix +\n"
+ ' "'
+ literal_value
+ '";'
)
plain_javascript_as_statement = (
"const pass"
+ "word = context.driverPass"
+ "word\n"
+ 'as("'
+ literal_value
+ '");'
)
typescript_dollar_identifier = (
"const pass"
+ "word = context.driverPass"
+ "word\n"
+ 'as$logger("'
+ literal_value
+ '");'
)
self.assertFalse(
self.helper["secret_text_risk"](
content,
javascript_dialect="typescript",
)
)
self.assertFalse(
self.helper["secret_text_risk"](
sut_reference,
javascript_dialect="typescript",
)
)
self.assertTrue(self.helper["secret_text_risk"](sut_reference))
self.assertFalse(
self.helper["secret_text_risk"](
plain_javascript_as_statement,
javascript_dialect="javascript",
)
)
self.assertFalse(
self.helper["secret_text_risk"](
typescript_dollar_identifier,
javascript_dialect="typescript",
)
)
self.assertFalse(
self.helper["secret_text_risk"](
final_property,
javascript_dialect="typescript",
)
)
for source_reference in (
inline_property,
asserted_property,
cast_property,
union_cast_property,
next_statement,
line_comment,
block_comment,
leading_comma_statement,
unary_statement,
continued_then_unary_statement,
):
with self.subTest(source_reference=source_reference):
self.assertFalse(
self.helper["secret_text_risk"](
source_reference,
javascript_dialect="typescript",
)
)
for unsafe_source_reference in (
concatenated_literal,
continued_literal,
fallback_literal,
assigned_literal,
newline_cast_literal,
commented_continuation,
unicode_comment_continuation,
unicode_space_continuation,
multiline_cast_literal,
inequality_literal,
member_call_literal,
trailing_operator_literal,
):
with self.subTest(unsafe_source_reference=unsafe_source_reference):
self.assertTrue(
self.helper["secret_text_risk"](
unsafe_source_reference,
javascript_dialect="typescript",
)
)
self.assertTrue(self.helper["secret_text_risk"](yaml_literal))
self.assertTrue(self.helper["secret_text_risk"](yaml_reference))
self.assertTrue(self.helper["secret_text_risk"](yaml_flow_reference))
self.assertTrue(self.helper["secret_text_risk"](source_literal))
self.assertTrue(self.helper["secret_text_risk"](undeclared_member))
self.assertTrue(self.helper["secret_text_risk"](declared_member))
self.assertTrue(self.helper["secret_text_risk"](prefixed_member))
self.assertTrue(
self.helper["secret_text_risk"](declared_identifier_value)
)
self.assertTrue(
self.helper["secret_text_risk"](
suffixed_reference,
javascript_dialect="typescript",
)
)
self.assertTrue(
self.helper["secret_text_risk"](
prefixed_reference,
javascript_dialect="typescript",
)
)
def test_review_patch_scopes_source_references_to_typescript_files(self) -> None:
property_name = "pass" + "word"
reference = "context.driverPass" + "word"
source_patch = (
"diff --git a/src/runtime.ts b/src/runtime.ts\n"
"--- a/src/runtime.ts\n"
"+++ b/src/runtime.ts\n"
"@@ -0,0 +1 @@\n"
"+function configure(context: RuntimeContext) { return { "
+ property_name
+ ": "
+ reference
+ " }; }\n"
)
narrow_source_patch = (
"diff --git a/src/runtime.ts b/src/runtime.ts\n"
"--- a/src/runtime.ts\n"
"+++ b/src/runtime.ts\n"
"@@ -40,2 +40,3 @@ function configure(context: RuntimeContext) {\n"
" return {\n"
"+ "
+ property_name
+ ": "
+ reference
+ ",\n"
" };\n"
)
config_patch = (
"diff --git a/config.yml b/config.yml\n"
"--- a/config.yml\n"
"+++ b/config.yml\n"
"@@ -0,0 +1 @@\n"
"+"
+ property_name
+ ": "
+ reference
+ "\n"
)
self.assertEqual(
self.helper["validate_review_patch"](
"local staged diff",
["src/runtime.ts"],
source_patch,
),
source_patch,
)
self.assertEqual(
self.helper["validate_review_patch"](
"local staged diff",
["src/runtime.ts"],
narrow_source_patch,
),
narrow_source_patch,
)
with self.assertRaisesRegex(SystemExit, "secret-like content"):
self.helper["validate_review_patch"](
"local staged diff",
["src/runtime.ts", "config.yml"],
source_patch + config_patch,
)
with self.assertRaisesRegex(SystemExit, "secret-like content"):
self.helper["validate_review_patch"](
"local staged diff",
["config.yml", "src/runtime.ts"],
source_patch + config_patch,
)
def test_review_patch_scans_rename_sides_with_their_own_file_types(self) -> None:
property_name = "pass" + "word"
reference = "context.driverPass" + "word"
patch = (
"diff --git a/src/runtime.ts b/config.yml\n"
"similarity index 80%\n"
"rename from src/runtime.ts\n"
"rename to config.yml\n"
"--- a/src/runtime.ts\n"
"+++ b/config.yml\n"
"@@ -1 +1 @@\n"
"-function configure(context: RuntimeContext) { return { "
+ property_name
+ ": "
+ reference
+ " }; }\n"
"+"
+ property_name
+ ": "
+ reference
+ "\n"
)
with self.assertRaisesRegex(SystemExit, "secret-like content"):
self.helper["validate_review_patch"](
"branch diff",
["src/runtime.ts", "config.yml"],
patch,
)
def test_review_patch_decodes_git_quoted_source_paths(self) -> None:
property_name = "pass" + "word"
reference = "context.driverPass" + "word"
patch = (
'diff --git "a/\\303\\251.ts" "b/\\303\\251.ts"\n'
'--- "a/\\303\\251.ts"\n'
'+++ "b/\\303\\251.ts"\n'
"@@ -40,2 +40,3 @@ function configure(context: RuntimeContext) {\n"
" return {\n"
"+ "
+ property_name
+ ": "
+ reference
+ ",\n"
" };\n"
)
self.assertEqual(
self.helper["validate_review_patch"](
"local staged diff",
["é.ts"],
patch,
),
patch,
)
self.assertEqual(
self.helper["javascript_review_dialect"]("module.mts"),
"typescript",
)
self.assertEqual(
self.helper["javascript_review_dialect"]("module.cts"),
"typescript",
)
def test_secret_detector_allows_generated_fixture_credentials(self) -> None:
property_name = "pass" + "word"
variable_name = "to" + "ken"
access_property = "access" + "Token"
generated_fixture = (
f"function register() {{ return {{ {property_name}: "
+ "`matrix-qa-${randomUUID()}` }; }"
)
generated_marker = (
f"const {variable_name} = "
+ 'buildMatrixQaToken("MATRIX_QA_E2EE_THREAD");'
)
decoy_fixture = (
f"const config = {{ {access_property}: "
+ 'decoy-'
+ 'token" };'
)
invalid_recovery_fixture = (
'const recoveryKey = "not-'
+ 'a-valid-matrix-recovery-key";'
)
literal_value = "actual-production-" + "secret"
fixture_shaped_literal = "PROD_TEST_ACTUAL_" + "SECRET_0123456789"
adversarial_label = "TEST_Q7WX9M2NK4PV8R6DH3JC"
unsafe_template = (
f"const {property_name} = "
+ "`prod-live-secret-${randomUUID()}`;"
)
unsafe_string_template = (
f"const {property_name} = "
+ "`prod-test-live-secret-${String()}`;"
)
unsafe_suffix_template = (
f"const {property_name} = "
+ "`prod-live-secret-test-${randomUUID()}`;"
)
unsafe_call = (
f"const {variable_name} = "
+ f'buildToken("{literal_value}");'
)
unsafe_fixture_label = (
f"const {property_name} = "
+ f'"{fixture_shaped_literal}";'
)
unsafe_generator_label = (
f"const {variable_name} = "
+ f'buildMatrixQaToken("{fixture_shaped_literal}");'
)
unsafe_identity_call = (
f"const {variable_name} = "
+ f'buildTestToken("{adversarial_label}");'
)
for content in (
generated_fixture,
generated_marker,
decoy_fixture,
invalid_recovery_fixture,
):
with self.subTest(content=content):
self.assertFalse(self.helper["secret_text_risk"](content))
for content in (
unsafe_template,
unsafe_string_template,
unsafe_suffix_template,
unsafe_call,
unsafe_fixture_label,
unsafe_generator_label,
unsafe_identity_call,
):
with self.subTest(content=content):
self.assertTrue(self.helper["secret_text_risk"](content))
def test_fallback_self_test_ignores_ambient_model_overrides(self) -> None:
with mock.patch.dict(
os.environ,

View File

@@ -273,7 +273,6 @@ extensions/qa-matrix/src/runners/contract/runtime.ts
extensions/qa-matrix/src/runners/contract/scenario-catalog.ts
extensions/qa-matrix/src/runners/contract/scenario-runtime-approval.ts
extensions/qa-matrix/src/runners/contract/scenario-runtime-e2ee-destructive.ts
extensions/qa-matrix/src/runners/contract/scenario-runtime-e2ee.ts
extensions/qa-matrix/src/runners/contract/scenario-runtime-room.ts
extensions/qa-matrix/src/runners/contract/scenarios.test.ts
extensions/qa-matrix/src/substrate/client.ts

View File

@@ -299,11 +299,12 @@ All `openclaw matrix` commands accept `--verbose` (full diagnostics), `--json` (
```bash
openclaw matrix encryption setup
printf '%s\n' "$MATRIX_RECOVERY_KEY" | openclaw matrix encryption setup --recovery-key-stdin
```
Bootstraps secret storage and cross-signing, creates a room-key backup if needed, then prints status and next steps. Useful flags:
- `--recovery-key <key>` apply a recovery key before bootstrapping (prefer the stdin form below)
- `--recovery-key-stdin` reads a recovery key from stdin without exposing it in process arguments; `--recovery-key <key>` remains available for compatibility
- `--force-reset-cross-signing` discard the current cross-signing identity and create a new one (intentional use only)
For a new account, enable E2EE at creation time:

View File

@@ -1183,7 +1183,7 @@ describe("matrix CLI verification commands", () => {
expect(console.log).toHaveBeenCalledWith("Matrix verification bootstrap: complete");
});
it("enables E2EE and prints verification status from matrix encryption setup", async () => {
it("reads the recovery key from stdin during matrix encryption setup", async () => {
const cfg = {
channels: {
matrix: {
@@ -1218,11 +1218,13 @@ describe("matrix CLI verification commands", () => {
mockMatrixVerificationStatus({
recoveryKeyCreatedAt: "2026-03-09T06:00:00.000Z",
});
mockRecoveryKeyStdin("stdin-recovery-key\n");
const program = buildProgram();
await program.parseAsync(["matrix", "encryption", "setup", "--account", "ops"], {
from: "user",
});
await program.parseAsync(
["matrix", "encryption", "setup", "--account", "ops", "--recovery-key-stdin"],
{ from: "user" },
);
const replaceArg = mockCallArg(matrixRuntimeReplaceConfigFileMock) as {
nextConfig?: CoreConfig;
@@ -1239,7 +1241,7 @@ describe("matrix CLI verification commands", () => {
};
expect(bootstrapArg.accountId).toBe("ops");
expect(bootstrapArg.cfg?.channels?.matrix?.accounts?.ops?.encryption).toBe(true);
expect(bootstrapArg.recoveryKey).toBeUndefined();
expect(bootstrapArg.recoveryKey).toBe("stdin-recovery-key");
expect(bootstrapArg.forceResetCrossSigning).toBe(false);
const statusArg = mockCallArg(getMatrixVerificationStatusMock) as Record<string, unknown>;
expect(statusArg.accountId).toBe("ops");

View File

@@ -1634,7 +1634,11 @@ export function registerMatrixCli(params: { program: Command }): void {
.command("setup")
.description("Enable Matrix E2EE, bootstrap verification, and print next steps")
.option("--account <id>", "Account ID (for multi-account setups)")
.option("--recovery-key <key>", "Recovery key to apply before bootstrap")
.option(
"--recovery-key <key>",
"Recovery key to apply before bootstrap (prefer --recovery-key-stdin)",
)
.option("--recovery-key-stdin", "Read the Matrix recovery key from stdin")
.option(
"--force-reset-cross-signing",
"Force reset cross-signing identity before bootstrap (requires active recovery key)",
@@ -1645,6 +1649,7 @@ export function registerMatrixCli(params: { program: Command }): void {
async (options: {
account?: string;
recoveryKey?: string;
recoveryKeyStdin?: boolean;
forceResetCrossSigning?: boolean;
verbose?: boolean;
json?: boolean;
@@ -1655,7 +1660,10 @@ export function registerMatrixCli(params: { program: Command }): void {
run: async () =>
await setupMatrixEncryption({
account: options.account,
recoveryKey: options.recoveryKey,
recoveryKey: await resolveMatrixCliRecoveryKeyInput({
recoveryKey: options.recoveryKey,
recoveryKeyStdin: options.recoveryKeyStdin,
}),
forceResetCrossSigning: options.forceResetCrossSigning === true,
}),
onText: (result, verbose) => {

View File

@@ -0,0 +1,417 @@
// Qa Matrix plugin module implements account setup CLI E2EE scenarios.
import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
import { createMatrixQaClient } from "../../substrate/client.js";
import { startMatrixQaFaultProxy } from "../../substrate/fault-proxy.js";
import {
assertMatrixQaCliAccountAddBootstrapStatus,
assertMatrixQaCliE2eeStatus,
buildMatrixQaCliE2eeAccountConfig,
runMatrixQaCliExpectedFailure,
} from "./scenario-runtime-e2ee-cli-config.js";
import { createMatrixQaCliE2eeSetupRuntime } from "./scenario-runtime-e2ee-cli-runtime.js";
import {
isMatrixQaCliBackupUsable,
parseMatrixQaCliJson,
registerMatrixQaCliE2eeAccount,
type MatrixQaCliAccountAddStatus,
type MatrixQaCliEncryptionSetupStatus,
type MatrixQaCliVerificationStatus,
writeMatrixQaCliOutputArtifacts,
} from "./scenario-runtime-e2ee-cli-shared.js";
import { buildRoomKeyBackupUnavailableFaultRule } from "./scenario-runtime-e2ee-room.js";
import { MATRIX_QA_ROOM_KEY_BACKUP_FAULT_RULE_ID } from "./scenario-runtime-e2ee-shared.js";
import type { MatrixQaScenarioContext } from "./scenario-runtime-shared.js";
import type { MatrixQaScenarioExecution } from "./scenario-types.js";
export async function runMatrixQaE2eeCliAccountAddEnableE2eeScenario(
context: MatrixQaScenarioContext,
): Promise<MatrixQaScenarioExecution> {
const accountId = "cli-add-e2ee";
const account = await registerMatrixQaCliE2eeAccount({
context,
deviceName: "OpenClaw Matrix QA CLI Account Add Owner",
scenarioId: "matrix-e2ee-cli-account-add-enable-e2ee",
});
const cli = await createMatrixQaCliE2eeSetupRuntime({
artifactLabel: "cli-account-add-enable-e2ee",
context,
});
try {
const addResult = await cli.run([
"matrix",
"account",
"add",
"--account",
accountId,
"--name",
"Matrix QA CLI Account Add E2EE",
"--homeserver",
context.baseUrl,
"--user-id",
account.userId,
"--password",
account.password,
"--device-name",
"OpenClaw Matrix QA CLI Account Add E2EE",
"--allow-private-network",
"--enable-e2ee",
"--json",
]);
const addArtifacts = await writeMatrixQaCliOutputArtifacts({
label: "account-add-enable-e2ee",
result: addResult,
rootDir: cli.rootDir,
});
const added = parseMatrixQaCliJson(addResult) as MatrixQaCliAccountAddStatus;
if (added.accountId !== accountId || added.encryptionEnabled !== true) {
throw new Error(
"Matrix CLI account add did not report E2EE enabled for the expected account",
);
}
if (added.verificationBootstrap?.attempted !== true) {
throw new Error("Matrix CLI account add did not attempt verification bootstrap");
}
if (added.verificationBootstrap.success !== true) {
throw new Error(
`Matrix CLI account add verification bootstrap failed: ${added.verificationBootstrap.error ?? "unknown error"}`,
);
}
const statusResult = await cli.run([
"matrix",
"verify",
"status",
"--account",
accountId,
"--allow-degraded-local-state",
"--json",
]);
const statusArtifacts = await writeMatrixQaCliOutputArtifacts({
label: "verify-status",
result: statusResult,
rootDir: cli.rootDir,
});
const status = parseMatrixQaCliJson(statusResult) as MatrixQaCliVerificationStatus;
assertMatrixQaCliAccountAddBootstrapStatus({
expectedBackupVersion: added.verificationBootstrap.backupVersion,
expectedUserId: account.userId,
status,
});
const cliDeviceId = status.deviceId ?? null;
return {
artifacts: {
accountId,
backupVersion: added.verificationBootstrap.backupVersion ?? null,
cliDeviceId,
encryptionEnabled: added.encryptionEnabled,
verificationBootstrapAttempted: added.verificationBootstrap.attempted,
verificationBootstrapSuccess: added.verificationBootstrap.success,
},
details: [
"Matrix CLI account add --enable-e2ee created an encrypted account and bootstrapped recovery state",
`account add stdout: ${addArtifacts.stdoutPath}`,
`account add stderr: ${addArtifacts.stderrPath}`,
`verify status stdout: ${statusArtifacts.stdoutPath}`,
`verify status stderr: ${statusArtifacts.stderrPath}`,
`cli device: ${cliDeviceId ?? "<unknown>"}`,
`cli verified by owner: ${status.verified ? "yes" : "no"}`,
`cli backup usable: ${isMatrixQaCliBackupUsable(status.backup) ? "yes" : "no"}`,
].join("\n"),
};
} finally {
await cli.dispose();
}
}
export async function runMatrixQaE2eeCliEncryptionSetupScenario(
context: MatrixQaScenarioContext,
): Promise<MatrixQaScenarioExecution> {
const accountId = "cli-encryption-setup";
const account = await registerMatrixQaCliE2eeAccount({
context,
deviceName: "OpenClaw Matrix QA CLI Encryption Setup Owner",
scenarioId: "matrix-e2ee-cli-encryption-setup",
});
const loginClient = createMatrixQaClient({
baseUrl: context.baseUrl,
});
const cliDevice = await loginClient.loginWithPassword({
deviceName: "OpenClaw Matrix QA CLI Encryption Setup Device",
password: account.password,
userId: account.userId,
});
if (!cliDevice.deviceId) {
throw new Error("Matrix E2EE CLI encryption setup login did not return a device id");
}
const cli = await createMatrixQaCliE2eeSetupRuntime({
artifactLabel: "cli-encryption-setup",
context,
initialConfig: buildMatrixQaCliE2eeAccountConfig({
accountId,
accessToken: cliDevice.accessToken,
baseUrl: context.baseUrl,
deviceId: cliDevice.deviceId,
encryption: false,
name: "Matrix QA CLI Encryption Setup",
password: account.password,
userId: cliDevice.userId,
}),
});
try {
const setupResult = await cli.run([
"matrix",
"encryption",
"setup",
"--account",
accountId,
"--json",
]);
const setupArtifacts = await writeMatrixQaCliOutputArtifacts({
label: "encryption-setup",
result: setupResult,
rootDir: cli.rootDir,
});
const setup = parseMatrixQaCliJson(setupResult) as MatrixQaCliEncryptionSetupStatus;
if (
setup.accountId !== accountId ||
setup.success !== true ||
setup.encryptionChanged !== true ||
setup.bootstrap?.success !== true ||
!setup.status
) {
throw new Error(
`Matrix CLI encryption setup did not report a successful E2EE upgrade: ${setup.bootstrap?.error ?? "unknown error"}`,
);
}
assertMatrixQaCliE2eeStatus("Matrix CLI encryption setup", setup.status);
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;
assertMatrixQaCliE2eeStatus("Matrix CLI encryption setup status", status);
return {
artifacts: {
accountId,
cliDeviceId: status.deviceId ?? cliDevice.deviceId,
encryptionChanged: setup.encryptionChanged,
setupSuccess: setup.success,
verificationBootstrapSuccess: setup.bootstrap.success,
},
details: [
"Matrix CLI encryption setup upgraded an existing account and bootstrapped verification",
`encryption setup stdout: ${setupArtifacts.stdoutPath}`,
`encryption setup stderr: ${setupArtifacts.stderrPath}`,
`verify status stdout: ${statusArtifacts.stdoutPath}`,
`verify status stderr: ${statusArtifacts.stderrPath}`,
`cli device: ${status.deviceId ?? cliDevice.deviceId}`,
`cli verified by owner: ${status.verified ? "yes" : "no"}`,
`cli backup usable: ${isMatrixQaCliBackupUsable(status.backup) ? "yes" : "no"}`,
].join("\n"),
};
} finally {
await cli.dispose();
}
}
export async function runMatrixQaE2eeCliEncryptionSetupIdempotentScenario(
context: MatrixQaScenarioContext,
): Promise<MatrixQaScenarioExecution> {
const accountId = "cli-encryption-idempotent";
const account = await registerMatrixQaCliE2eeAccount({
context,
deviceName: "OpenClaw Matrix QA CLI Encryption Idempotent Owner",
scenarioId: "matrix-e2ee-cli-encryption-setup-idempotent",
});
const loginClient = createMatrixQaClient({
baseUrl: context.baseUrl,
});
const cliDevice = await loginClient.loginWithPassword({
deviceName: "OpenClaw Matrix QA CLI Encryption Idempotent Device",
password: account.password,
userId: account.userId,
});
if (!cliDevice.deviceId) {
throw new Error("Matrix E2EE CLI idempotent setup login did not return a device id");
}
const cli = await createMatrixQaCliE2eeSetupRuntime({
artifactLabel: "cli-encryption-setup-idempotent",
context,
initialConfig: buildMatrixQaCliE2eeAccountConfig({
accountId,
accessToken: cliDevice.accessToken,
baseUrl: context.baseUrl,
deviceId: cliDevice.deviceId,
encryption: true,
name: "Matrix QA CLI Encryption Setup Idempotent",
password: account.password,
userId: cliDevice.userId,
}),
});
try {
const setupArgs = ["matrix", "encryption", "setup", "--account", accountId, "--json"];
const firstResult = await cli.run(setupArgs);
const firstArtifacts = await writeMatrixQaCliOutputArtifacts({
label: "encryption-setup-first",
result: firstResult,
rootDir: cli.rootDir,
});
const first = parseMatrixQaCliJson(firstResult) as MatrixQaCliEncryptionSetupStatus;
if (
first.accountId !== accountId ||
first.success !== true ||
first.encryptionChanged !== false ||
first.bootstrap?.success !== true ||
!first.status
) {
throw new Error(
`Matrix CLI encryption setup was not idempotent on first run: ${first.bootstrap?.error ?? "unknown error"}`,
);
}
assertMatrixQaCliE2eeStatus("Matrix CLI encryption setup idempotent first run", first.status);
const secondResult = await cli.run(setupArgs);
const secondArtifacts = await writeMatrixQaCliOutputArtifacts({
label: "encryption-setup-second",
result: secondResult,
rootDir: cli.rootDir,
});
const second = parseMatrixQaCliJson(secondResult) as MatrixQaCliEncryptionSetupStatus;
if (
second.accountId !== accountId ||
second.success !== true ||
second.encryptionChanged !== false ||
second.bootstrap?.success !== true ||
!second.status
) {
throw new Error(
`Matrix CLI encryption setup was not idempotent on second run: ${second.bootstrap?.error ?? "unknown error"}`,
);
}
assertMatrixQaCliE2eeStatus("Matrix CLI encryption setup idempotent second run", second.status);
return {
artifacts: {
accountId,
cliDeviceId: second.status.deviceId ?? cliDevice.deviceId,
firstEncryptionChanged: first.encryptionChanged,
secondEncryptionChanged: second.encryptionChanged,
setupSuccess: second.success,
verificationBootstrapSuccess: second.bootstrap.success,
},
details: [
"Matrix CLI encryption setup stayed idempotent on an already encrypted account",
`first setup stdout: ${firstArtifacts.stdoutPath}`,
`first setup stderr: ${firstArtifacts.stderrPath}`,
`second setup stdout: ${secondArtifacts.stdoutPath}`,
`second setup stderr: ${secondArtifacts.stderrPath}`,
`cli device: ${second.status.deviceId ?? cliDevice.deviceId}`,
`first encryption changed: ${first.encryptionChanged ? "yes" : "no"}`,
`second encryption changed: ${second.encryptionChanged ? "yes" : "no"}`,
].join("\n"),
};
} finally {
await cli.dispose();
}
}
export async function runMatrixQaE2eeCliEncryptionSetupBootstrapFailureScenario(
context: MatrixQaScenarioContext,
): Promise<MatrixQaScenarioExecution> {
const accountId = "cli-encryption-failure";
const account = await registerMatrixQaCliE2eeAccount({
context,
deviceName: "OpenClaw Matrix QA CLI Encryption Failure Owner",
scenarioId: "matrix-e2ee-cli-encryption-setup-bootstrap-failure",
});
const loginClient = createMatrixQaClient({
baseUrl: context.baseUrl,
});
const cliDevice = await loginClient.loginWithPassword({
deviceName: "OpenClaw Matrix QA CLI Encryption Failure Device",
password: account.password,
userId: account.userId,
});
if (!cliDevice.deviceId) {
throw new Error("Matrix E2EE CLI bootstrap-failure login did not return a device id");
}
const proxy = await startMatrixQaFaultProxy({
targetBaseUrl: context.faultProxyTargetBaseUrl ?? context.baseUrl,
...context.faultProxyObserver,
rules: [buildRoomKeyBackupUnavailableFaultRule(cliDevice.accessToken)],
});
const cli = await createMatrixQaCliE2eeSetupRuntime({
artifactLabel: "cli-encryption-setup-bootstrap-failure",
context,
initialConfig: buildMatrixQaCliE2eeAccountConfig({
accountId,
accessToken: cliDevice.accessToken,
baseUrl: proxy.baseUrl,
deviceId: cliDevice.deviceId,
encryption: false,
name: "Matrix QA CLI Encryption Setup Bootstrap Failure",
password: account.password,
userId: cliDevice.userId,
}),
});
try {
const failed = await runMatrixQaCliExpectedFailure({
args: ["matrix", "encryption", "setup", "--account", accountId, "--json"],
start: cli.start,
timeoutMs: context.timeoutMs,
});
const artifacts = await writeMatrixQaCliOutputArtifacts({
label: "encryption-setup-bootstrap-failure",
result: failed,
rootDir: cli.rootDir,
});
const payload = parseMatrixQaCliJson(failed) as MatrixQaCliEncryptionSetupStatus;
if (payload.success !== false && payload.bootstrap?.success !== false) {
throw new Error("Matrix CLI encryption setup failure did not report unsuccessful bootstrap");
}
const faultHits = proxy.hits();
if (faultHits.length === 0) {
throw new Error("Matrix CLI encryption setup bootstrap-failure proxy was not exercised");
}
const bootstrapError = payload.bootstrap?.error ?? "";
if (!bootstrapError.toLowerCase().includes("room key backup")) {
throw new Error(
`Matrix CLI encryption setup failed for an unexpected reason: ${bootstrapError}`,
);
}
return {
artifacts: {
accountId,
bootstrapErrorPreview: truncateUtf16Safe(bootstrapError, 240),
bootstrapSuccess: false,
cliDeviceId: cliDevice.deviceId,
faultedEndpoint: faultHits[0]?.path,
faultHitCount: faultHits.length,
faultRuleId: MATRIX_QA_ROOM_KEY_BACKUP_FAULT_RULE_ID,
},
details: [
"Matrix CLI encryption setup surfaced a bootstrap failure from a faulted room-key backup endpoint",
`failure stdout: ${artifacts.stdoutPath}`,
`failure stderr: ${artifacts.stderrPath}`,
`fault hits: ${faultHits.length}`,
`fault endpoint: ${faultHits[0]?.path ?? "<none>"}`,
`bootstrap error: ${bootstrapError}`,
].join("\n"),
};
} finally {
await Promise.all([cli.dispose(), proxy.stop().catch(() => undefined)]);
}
}

View File

@@ -0,0 +1,162 @@
// Qa Matrix plugin module implements CLI config assertions for E2EE scenarios.
import { readFile } from "node:fs/promises";
import {
formatMatrixQaCliCommand,
redactMatrixQaCliOutput,
type MatrixQaCliRunResult,
type MatrixQaCliSession,
} from "./scenario-runtime-cli.js";
import {
buildMatrixQaPluginActivationConfig,
isMatrixQaCliBackupUsable,
type MatrixQaCliVerificationStatus,
} from "./scenario-runtime-e2ee-cli-shared.js";
export function assertMatrixQaCliE2eeStatus(
label: string,
status: MatrixQaCliVerificationStatus,
opts: { allowUntrustedMatchingKey?: boolean } = {},
) {
if (
status.verified !== true ||
status.crossSigningVerified !== true ||
status.signedByOwner !== true ||
!isMatrixQaCliBackupUsable(status.backup, opts)
) {
throw new Error(
`${label} did not leave the CLI account fully verified and backup-usable: ownerVerified=${
status.verified === true &&
status.crossSigningVerified === true &&
status.signedByOwner === true
? "yes"
: "no"
}, backupUsable=${isMatrixQaCliBackupUsable(status.backup, opts) ? "yes" : "no"}${
status.backup?.keyLoadError ? `, backupError=${status.backup.keyLoadError}` : ""
}`,
);
}
}
export function assertMatrixQaCliAccountAddBootstrapStatus(params: {
expectedBackupVersion?: string | null;
expectedUserId: string;
status: MatrixQaCliVerificationStatus;
}) {
const { expectedBackupVersion, expectedUserId, status } = params;
if (status.encryptionEnabled !== true || status.recoveryKeyStored !== true) {
throw new Error(
"Matrix CLI account add --enable-e2ee degraded status did not keep encryption and recovery key state",
);
}
if (status.userId !== expectedUserId) {
throw new Error(
`Matrix CLI account add --enable-e2ee status user mismatch: expected ${expectedUserId}, got ${status.userId ?? "<none>"}`,
);
}
if (!status.deviceId || status.serverDeviceKnown !== true) {
throw new Error(
"Matrix CLI account add --enable-e2ee degraded status did not resolve the current server-known device",
);
}
if (expectedBackupVersion && status.backupVersion !== expectedBackupVersion) {
throw new Error(
`Matrix CLI account add --enable-e2ee backup version mismatch: expected ${expectedBackupVersion}, got ${status.backupVersion ?? "<none>"}`,
);
}
if (status.backup?.keyLoadError) {
throw new Error(
`Matrix CLI account add --enable-e2ee degraded status reported backup key error: ${status.backup.keyLoadError}`,
);
}
}
export async function runMatrixQaCliExpectedFailure(params: {
args: string[];
start: (args: string[], timeoutMs?: number) => MatrixQaCliSession;
stdin?: string;
timeoutMs: number;
}): Promise<MatrixQaCliRunResult> {
const session = params.start(params.args, params.timeoutMs);
try {
if (params.stdin !== undefined) {
await session.writeStdin(params.stdin);
session.endStdin();
}
const result = await session.wait();
throw new Error(
`${formatMatrixQaCliCommand(params.args)} unexpectedly succeeded with stdout:\n${redactMatrixQaCliOutput(
result.stdout,
)}`,
);
} catch (error) {
if (error instanceof Error && error.message.includes("unexpectedly succeeded")) {
throw error;
}
const output = session.output();
if (!output.stdout.trim() && !output.stderr.trim()) {
throw error;
}
return {
args: params.args,
exitCode: 1,
stderr: output.stderr,
stdout: output.stdout,
};
} finally {
session.kill();
}
}
export function buildMatrixQaCliE2eeAccountConfig(params: {
accountId: string;
accessToken: string;
baseUrl: string;
deviceId: string;
encryption: boolean;
name: string;
password?: string;
userId: string;
}) {
return {
...buildMatrixQaPluginActivationConfig(),
channels: {
matrix: {
defaultAccount: params.accountId,
accounts: {
[params.accountId]: {
accessToken: params.accessToken,
deviceId: params.deviceId,
encryption: params.encryption,
homeserver: params.baseUrl,
initialSyncLimit: 1,
name: params.name,
network: {
dangerouslyAllowPrivateNetwork: true,
},
...(params.password ? { password: params.password } : {}),
startupVerification: "off",
userId: params.userId,
},
},
},
},
};
}
export async function readMatrixQaCliConfig(pathname: string): Promise<{
channels?: {
matrix?: {
accounts?: Record<string, Record<string, unknown>>;
defaultAccount?: string;
};
};
}> {
return JSON.parse(await readFile(pathname, "utf8")) as {
channels?: {
matrix?: {
accounts?: Record<string, Record<string, unknown>>;
defaultAccount?: string;
};
};
};
}

View File

@@ -0,0 +1,384 @@
// Qa Matrix plugin module implements gateway CLI E2EE scenarios.
import { randomUUID } from "node:crypto";
import { createMatrixQaClient } from "../../substrate/client.js";
import { createMatrixQaE2eeScenarioClient } from "../../substrate/e2ee-client.js";
import { buildMatrixQaE2eeScenarioRoomKey } from "./scenario-catalog.js";
import {
patchMatrixQaGatewayMatrixAccount,
replaceMatrixQaGatewayMatrixAccount,
} from "./scenario-runtime-config.js";
import {
assertMatrixQaCliE2eeStatus,
readMatrixQaCliConfig,
} from "./scenario-runtime-e2ee-cli-config.js";
import {
createMatrixQaCliE2eeSetupRuntime,
createMatrixQaCliGatewayRuntime,
} from "./scenario-runtime-e2ee-cli-runtime.js";
import {
buildMatrixQaPluginActivationConfig,
parseMatrixQaCliJson,
registerMatrixQaCliE2eeAccount,
type MatrixQaCliEncryptionSetupStatus,
writeMatrixQaCliOutputArtifacts,
} from "./scenario-runtime-e2ee-cli-shared.js";
import { buildMatrixE2eeReplyArtifact } from "./scenario-runtime-e2ee-room.js";
import {
ensureMatrixQaE2eeOwnDeviceVerified,
requireMatrixQaE2eeOutputDir,
requireMatrixQaGatewayConfigPath,
} from "./scenario-runtime-e2ee-shared.js";
import {
assertTopLevelReplyArtifact,
buildMatrixQaToken,
buildMatrixReplyDetails,
buildMentionPrompt,
isMatrixQaExactMarkerReply,
type MatrixQaScenarioContext,
} from "./scenario-runtime-shared.js";
import type { MatrixQaScenarioExecution } from "./scenario-types.js";
export async function runMatrixQaE2eeCliEncryptionSetupMultiAccountScenario(
context: MatrixQaScenarioContext,
): Promise<MatrixQaScenarioExecution> {
const accountId = "cli-multi-target";
const decoyAccountId = "cli-multi-decoy";
const account = await registerMatrixQaCliE2eeAccount({
context,
deviceName: "OpenClaw Matrix QA CLI Multi Account Owner",
scenarioId: "matrix-e2ee-cli-encryption-setup-multi-account",
});
const loginClient = createMatrixQaClient({
baseUrl: context.baseUrl,
});
const cliDevice = await loginClient.loginWithPassword({
deviceName: "OpenClaw Matrix QA CLI Multi Account Target Device",
password: account.password,
userId: account.userId,
});
if (!cliDevice.deviceId) {
throw new Error("Matrix E2EE CLI multi-account setup login did not return a device id");
}
const cli = await createMatrixQaCliE2eeSetupRuntime({
artifactLabel: "cli-encryption-setup-multi-account",
context,
initialConfig: {
...buildMatrixQaPluginActivationConfig(),
channels: {
matrix: {
defaultAccount: decoyAccountId,
accounts: {
[decoyAccountId]: {
accessToken: "decoy-token",
deviceId: "DECOYDEVICE",
encryption: false,
homeserver: context.baseUrl,
initialSyncLimit: 1,
name: "Matrix QA CLI Multi Account Decoy",
startupVerification: "off",
userId: "@decoy:matrix-qa.test",
},
[accountId]: {
accessToken: cliDevice.accessToken,
deviceId: cliDevice.deviceId,
encryption: false,
homeserver: context.baseUrl,
initialSyncLimit: 1,
name: "Matrix QA CLI Multi Account Target",
network: {
dangerouslyAllowPrivateNetwork: true,
},
password: account.password,
startupVerification: "off",
userId: cliDevice.userId,
},
},
},
},
},
});
try {
const setupResult = await cli.run([
"matrix",
"encryption",
"setup",
"--account",
accountId,
"--json",
]);
const setupArtifacts = await writeMatrixQaCliOutputArtifacts({
label: "encryption-setup-multi-account",
result: setupResult,
rootDir: cli.rootDir,
});
const setup = parseMatrixQaCliJson(setupResult) as MatrixQaCliEncryptionSetupStatus;
if (
setup.accountId !== accountId ||
setup.success !== true ||
setup.encryptionChanged !== true ||
setup.bootstrap?.success !== true ||
!setup.status
) {
throw new Error(
`Matrix CLI multi-account encryption setup did not target the requested account: ${setup.bootstrap?.error ?? "unknown error"}`,
);
}
assertMatrixQaCliE2eeStatus("Matrix CLI multi-account encryption setup", setup.status);
const config = await readMatrixQaCliConfig(cli.configPath);
const matrix = config.channels?.matrix;
const target = matrix?.accounts?.[accountId];
const decoy = matrix?.accounts?.[decoyAccountId];
const defaultAccountPreserved = matrix?.defaultAccount === decoyAccountId;
const decoyAccountPreserved =
decoy?.encryption === false &&
decoy?.accessToken === "decoy-token" &&
decoy?.deviceId === "DECOYDEVICE";
if (!defaultAccountPreserved) {
throw new Error("Matrix CLI multi-account setup changed the default account");
}
if (!decoyAccountPreserved) {
throw new Error("Matrix CLI multi-account setup mutated the decoy account");
}
if (target?.encryption !== true) {
throw new Error("Matrix CLI multi-account setup did not enable encryption on the target");
}
return {
artifacts: {
accountId,
cliDeviceId: setup.status.deviceId ?? cliDevice.deviceId,
decoyAccountPreserved,
defaultAccountPreserved,
encryptionChanged: setup.encryptionChanged,
setupSuccess: setup.success,
verificationBootstrapSuccess: setup.bootstrap.success,
},
details: [
"Matrix CLI encryption setup changed only the requested account in a multi-account config",
`setup stdout: ${setupArtifacts.stdoutPath}`,
`setup stderr: ${setupArtifacts.stderrPath}`,
`default account preserved: ${defaultAccountPreserved ? "yes" : "no"}`,
`decoy account preserved: ${decoyAccountPreserved ? "yes" : "no"}`,
`cli device: ${setup.status.deviceId ?? cliDevice.deviceId}`,
].join("\n"),
};
} finally {
await cli.dispose();
}
}
export async function runMatrixQaE2eeCliSetupThenGatewayReplyScenario(
context: MatrixQaScenarioContext,
): Promise<MatrixQaScenarioExecution> {
if (!context.restartGatewayAfterStateMutation) {
throw new Error(
"Matrix CLI setup gateway reply scenario requires hard gateway restart support",
);
}
const gatewayConfigPath = requireMatrixQaGatewayConfigPath(context);
const accountId = "cli-setup-gateway";
const scenarioId = "matrix-e2ee-cli-setup-then-gateway-reply";
const roomKey = buildMatrixQaE2eeScenarioRoomKey(scenarioId);
const account = await registerMatrixQaCliE2eeAccount({
context,
deviceName: "OpenClaw Matrix QA CLI Setup Gateway",
scenarioId,
});
const driverAccount = await registerMatrixQaCliE2eeAccount({
context,
deviceName: "OpenClaw Matrix QA CLI Setup Driver",
scenarioId,
});
const driverApi = createMatrixQaClient({
accessToken: driverAccount.accessToken,
baseUrl: context.baseUrl,
});
const gatewayApi = createMatrixQaClient({
accessToken: account.accessToken,
baseUrl: context.baseUrl,
});
const roomId = await driverApi.createPrivateRoom({
encrypted: true,
inviteUserIds: [account.userId],
name: "Matrix QA CLI Setup Gateway E2EE",
});
await gatewayApi.joinRoom(roomId);
const accountConfig = {
accessToken: account.accessToken,
deviceId: account.deviceId,
dm: {
allowFrom: [driverAccount.userId],
enabled: true,
policy: "allowlist",
sessionScope: "per-room",
threadReplies: "inbound",
},
enabled: true,
encryption: false,
groupAllowFrom: [driverAccount.userId],
groupPolicy: "allowlist",
groups: {
[roomId]: {
enabled: true,
requireMention: true,
},
},
homeserver: context.baseUrl,
initialSyncLimit: 1,
name: "Matrix QA CLI Setup Gateway",
network: {
dangerouslyAllowPrivateNetwork: true,
},
password: account.password,
startupVerification: "off",
threadReplies: "inbound",
userId: account.userId,
};
await context.restartGatewayAfterStateMutation(
async () => {
await replaceMatrixQaGatewayMatrixAccount({
accountConfig,
accountId,
configPath: gatewayConfigPath,
});
},
{
timeoutMs: context.timeoutMs,
waitAccountId: accountId,
},
);
await context.waitGatewayAccountReady?.(accountId, {
timeoutMs: context.timeoutMs,
});
const cli = await createMatrixQaCliGatewayRuntime({
artifactLabel: "cli-setup-then-gateway-reply",
context,
});
try {
const setupResult = await cli.run([
"matrix",
"encryption",
"setup",
"--account",
accountId,
"--json",
]);
const setupArtifacts = await writeMatrixQaCliOutputArtifacts({
label: "encryption-setup",
result: setupResult,
rootDir: cli.rootDir,
});
const setup = parseMatrixQaCliJson(setupResult) as MatrixQaCliEncryptionSetupStatus;
if (
setup.accountId !== accountId ||
setup.success !== true ||
setup.bootstrap?.success !== true
) {
throw new Error(
`Matrix CLI gateway account setup did not succeed: ${setup.bootstrap?.error ?? "unknown error"}`,
);
}
if (setup.status) {
assertMatrixQaCliE2eeStatus("Matrix CLI gateway account setup", setup.status);
}
await context.restartGatewayAfterStateMutation(
async () => {
await patchMatrixQaGatewayMatrixAccount({
accountPatch: {
encryption: true,
password: account.password,
},
accountId,
configPath: gatewayConfigPath,
});
},
{
timeoutMs: context.timeoutMs,
waitAccountId: accountId,
},
);
await context.waitGatewayAccountReady?.(accountId, {
timeoutMs: context.timeoutMs,
});
const driverClient = await createMatrixQaE2eeScenarioClient({
accessToken: driverAccount.accessToken,
actorId: `driver-cli-setup-gateway-${randomUUID().slice(0, 8)}`,
baseUrl: context.baseUrl,
deviceId: driverAccount.deviceId,
observedEvents: context.observedEvents,
outputDir: requireMatrixQaE2eeOutputDir(context),
password: driverAccount.password,
scenarioId,
timeoutMs: context.timeoutMs,
userId: driverAccount.userId,
});
const replied = await (async () => {
try {
await ensureMatrixQaE2eeOwnDeviceVerified({
client: driverClient,
label: "Matrix CLI setup scenario driver",
});
await driverClient.waitForJoinedMember({
roomId,
timeoutMs: context.timeoutMs,
userId: account.userId,
});
await driverClient.prime();
const token = buildMatrixQaToken("MATRIX_QA_E2EE_CLI_GATEWAY");
const driverEventId = await driverClient.sendTextMessage({
body: buildMentionPrompt(account.userId, token),
mentionUserIds: [account.userId],
roomId,
});
const matched = await driverClient.waitForRoomEvent({
predicate: (event) =>
isMatrixQaExactMarkerReply(event, {
roomId,
sutUserId: account.userId,
token,
}) && event.relatesTo === undefined,
roomId,
timeoutMs: context.timeoutMs,
});
const reply = buildMatrixE2eeReplyArtifact(matched.event, token);
assertTopLevelReplyArtifact("gateway reply", reply);
return {
driverEventId,
reply,
};
} finally {
await driverClient.stop();
}
})();
return {
artifacts: {
accountId,
cliDeviceId: setup.status?.deviceId ?? account.deviceId ?? null,
driverUserId: driverAccount.userId,
encryptionChanged: setup.encryptionChanged,
gatewayReply: replied.reply,
gatewayUserId: account.userId,
roomKey,
roomId,
setupSuccess: setup.success,
verificationBootstrapSuccess: setup.bootstrap.success,
},
details: [
"Matrix CLI encryption setup left the gateway able to reply in an encrypted room",
`setup stdout: ${setupArtifacts.stdoutPath}`,
`setup stderr: ${setupArtifacts.stderrPath}`,
`driver user: ${driverAccount.userId}`,
`gateway user: ${account.userId}`,
`encrypted room key: ${roomKey}`,
`encrypted room id: ${roomId}`,
`driver event: ${replied.driverEventId}`,
...buildMatrixReplyDetails("gateway reply", replied.reply),
].join("\n"),
};
} finally {
await cli.dispose();
}
}

View File

@@ -0,0 +1,246 @@
// Qa Matrix plugin module implements recovery-key CLI E2EE scenarios.
import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
import { createMatrixQaClient } from "../../substrate/client.js";
import {
assertMatrixQaCliE2eeStatus,
buildMatrixQaCliE2eeAccountConfig,
runMatrixQaCliExpectedFailure,
} from "./scenario-runtime-e2ee-cli-config.js";
import { createMatrixQaCliE2eeSetupRuntime } from "./scenario-runtime-e2ee-cli-runtime.js";
import {
createMatrixQaE2eeCliOwnerClient,
isMatrixQaCliBackupUsable,
parseMatrixQaCliJson,
registerMatrixQaCliE2eeAccount,
type MatrixQaCliEncryptionSetupStatus,
writeMatrixQaCliOutputArtifacts,
} from "./scenario-runtime-e2ee-cli-shared.js";
import { ensureMatrixQaE2eeOwnDeviceVerified } from "./scenario-runtime-e2ee-shared.js";
import type { MatrixQaScenarioContext } from "./scenario-runtime-shared.js";
import type { MatrixQaScenarioExecution } from "./scenario-types.js";
export async function runMatrixQaE2eeCliRecoveryKeySetupScenario(
context: MatrixQaScenarioContext,
): Promise<MatrixQaScenarioExecution> {
const accountId = "cli-recovery-key-setup";
const account = await registerMatrixQaCliE2eeAccount({
context,
deviceName: "OpenClaw Matrix QA CLI Recovery Key Owner",
scenarioId: "matrix-e2ee-cli-recovery-key-setup",
});
const owner = await createMatrixQaE2eeCliOwnerClient({
account,
context,
scenarioId: "matrix-e2ee-cli-recovery-key-setup",
});
const loginClient = createMatrixQaClient({
baseUrl: context.baseUrl,
});
const ready = await ensureMatrixQaE2eeOwnDeviceVerified({
client: owner,
label: "driver",
});
const encodedRecoveryKey = ready.recoveryKey?.encodedPrivateKey?.trim();
if (!encodedRecoveryKey) {
await owner.stop().catch(() => undefined);
throw new Error("Matrix E2EE CLI recovery-key setup did not expose a recovery key");
}
const cliDevice = await loginClient.loginWithPassword({
deviceName: "OpenClaw Matrix QA CLI Recovery Key Setup Device",
password: account.password,
userId: account.userId,
});
if (!cliDevice.deviceId) {
await owner.stop().catch(() => undefined);
throw new Error("Matrix E2EE CLI recovery-key setup login did not return a device id");
}
const cli = await createMatrixQaCliE2eeSetupRuntime({
artifactLabel: "cli-recovery-key-setup",
context,
initialConfig: buildMatrixQaCliE2eeAccountConfig({
accountId,
accessToken: cliDevice.accessToken,
baseUrl: context.baseUrl,
deviceId: cliDevice.deviceId,
encryption: false,
name: "Matrix QA CLI Recovery Key Setup",
password: account.password,
userId: cliDevice.userId,
}),
});
try {
const setupResult = await cli.run(
["matrix", "encryption", "setup", "--account", accountId, "--recovery-key-stdin", "--json"],
context.timeoutMs,
`${encodedRecoveryKey}\n`,
);
const setupArtifacts = await writeMatrixQaCliOutputArtifacts({
label: "recovery-key-setup",
result: setupResult,
rootDir: cli.rootDir,
});
const setup = parseMatrixQaCliJson(setupResult) as MatrixQaCliEncryptionSetupStatus;
if (
setup.accountId !== accountId ||
setup.success !== true ||
setup.encryptionChanged !== true ||
setup.bootstrap?.success !== true ||
!setup.status
) {
throw new Error(
`Matrix CLI recovery-key encryption setup did not succeed: ${setup.bootstrap?.error ?? "unknown error"}`,
);
}
assertMatrixQaCliE2eeStatus("Matrix CLI recovery-key encryption setup", setup.status, {
allowUntrustedMatchingKey: true,
});
return {
artifacts: {
accountId,
backupVersion: setup.status.backupVersion ?? ready.verification.backupVersion ?? null,
cliDeviceId: setup.status.deviceId ?? cliDevice.deviceId,
encryptionChanged: setup.encryptionChanged,
recoveryKeyId: ready.recoveryKey?.keyId ?? null,
recoveryKeyStored: true,
setupSuccess: setup.success,
verificationBootstrapSuccess: setup.bootstrap.success,
},
details: [
"Matrix CLI encryption setup accepted a recovery key on a second device",
`recovery setup stdout: ${setupArtifacts.stdoutPath}`,
`recovery setup stderr: ${setupArtifacts.stderrPath}`,
`owner backup version: ${ready.verification.backupVersion ?? "<none>"}`,
`recovery key id: ${ready.recoveryKey?.keyId ?? "<none>"}`,
`cli device: ${setup.status.deviceId ?? cliDevice.deviceId}`,
`cli verified by owner: ${setup.status.verified ? "yes" : "no"}`,
`cli backup usable: ${
isMatrixQaCliBackupUsable(setup.status.backup, { allowUntrustedMatchingKey: true })
? "yes"
: "no"
}`,
].join("\n"),
};
} finally {
try {
await owner.stop().catch(() => undefined);
await owner.deleteOwnDevices([cliDevice.deviceId]).catch(() => undefined);
} finally {
await cli.dispose();
}
}
}
export async function runMatrixQaE2eeCliRecoveryKeyInvalidScenario(
context: MatrixQaScenarioContext,
): Promise<MatrixQaScenarioExecution> {
const accountId = "cli-invalid-recovery-key";
const invalidRecoveryKey = "not-a-valid-matrix-recovery-key";
const account = await registerMatrixQaCliE2eeAccount({
context,
deviceName: "OpenClaw Matrix QA CLI Invalid Recovery Key Owner",
scenarioId: "matrix-e2ee-cli-recovery-key-invalid",
});
const owner = await createMatrixQaE2eeCliOwnerClient({
account,
context,
scenarioId: "matrix-e2ee-cli-recovery-key-invalid",
});
const ready = await ensureMatrixQaE2eeOwnDeviceVerified({
client: owner,
label: "cli invalid recovery-key owner",
});
if (!ready.recoveryKey?.encodedPrivateKey?.trim()) {
await owner.stop().catch(() => undefined);
throw new Error("Matrix E2EE CLI invalid recovery-key setup did not seed secret storage");
}
const loginClient = createMatrixQaClient({
baseUrl: context.baseUrl,
});
const cliDevice = await loginClient.loginWithPassword({
deviceName: "OpenClaw Matrix QA CLI Invalid Recovery Key Device",
password: account.password,
userId: account.userId,
});
if (!cliDevice.deviceId) {
await owner.stop().catch(() => undefined);
throw new Error("Matrix E2EE CLI invalid recovery-key login did not return a device id");
}
const cli = await createMatrixQaCliE2eeSetupRuntime({
artifactLabel: "cli-recovery-key-invalid",
context,
initialConfig: buildMatrixQaCliE2eeAccountConfig({
accountId,
accessToken: cliDevice.accessToken,
baseUrl: context.baseUrl,
deviceId: cliDevice.deviceId,
encryption: false,
name: "Matrix QA CLI Invalid Recovery Key",
password: account.password,
userId: cliDevice.userId,
}),
});
try {
const failed = await runMatrixQaCliExpectedFailure({
args: [
"matrix",
"encryption",
"setup",
"--account",
accountId,
"--recovery-key-stdin",
"--json",
],
start: cli.start,
stdin: `${invalidRecoveryKey}\n`,
timeoutMs: context.timeoutMs,
});
const artifacts = await writeMatrixQaCliOutputArtifacts({
label: "recovery-key-invalid",
result: failed,
rootDir: cli.rootDir,
});
const payload = parseMatrixQaCliJson(failed) as MatrixQaCliEncryptionSetupStatus & {
error?: string;
};
if (payload.success !== false && payload.bootstrap?.success !== false) {
throw new Error("Matrix CLI invalid recovery-key setup did not report failure");
}
const failure = payload.bootstrap?.error ?? payload.error ?? "";
if (!/recovery|secret|key/i.test(failure)) {
throw new Error(
`Matrix CLI invalid recovery-key setup failed for an unexpected reason: ${failure}`,
);
}
if (failed.stdout.includes(invalidRecoveryKey) || failed.stderr.includes(invalidRecoveryKey)) {
throw new Error("Matrix CLI invalid recovery-key output leaked the recovery key");
}
return {
artifacts: {
accountId,
bootstrapErrorPreview: truncateUtf16Safe(failure, 240),
bootstrapSuccess: false,
cliDeviceId: cliDevice.deviceId,
encryptionChanged: payload.encryptionChanged,
recoveryKeyAccepted: false,
recoveryKeyRejected: true,
setupSuccess: false,
},
details: [
"Matrix CLI encryption setup rejected an invalid recovery key without leaking it",
`failure stdout: ${artifacts.stdoutPath}`,
`failure stderr: ${artifacts.stderrPath}`,
`cli device: ${cliDevice.deviceId}`,
`failure: ${failure}`,
].join("\n"),
};
} finally {
try {
await owner.stop().catch(() => undefined);
await owner.deleteOwnDevices([cliDevice.deviceId]).catch(() => undefined);
} finally {
await cli.dispose();
}
}
}

View File

@@ -0,0 +1,206 @@
// Qa Matrix plugin module implements CLI runtime setup for E2EE scenarios.
import { randomUUID } from "node:crypto";
import { chmod, mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
import path from "node:path";
import { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk/temp-path";
import { runMatrixQaOpenClawCli, startMatrixQaOpenClawCli } from "./scenario-runtime-cli.js";
import {
assertMatrixQaPrivatePathMode,
buildMatrixQaEmptyMatrixCliConfig,
} from "./scenario-runtime-e2ee-cli-shared.js";
import {
requireMatrixQaCliRuntimeEnv,
requireMatrixQaE2eeOutputDir,
} from "./scenario-runtime-e2ee-shared.js";
import type { MatrixQaScenarioContext } from "./scenario-runtime-shared.js";
export async function createMatrixQaCliSelfVerificationRuntime(params: {
accountId: string;
accessToken: string;
context: MatrixQaScenarioContext;
deviceId: string;
userId: string;
}) {
const outputDir = requireMatrixQaE2eeOutputDir(params.context);
const rootDir = await mkdtemp(
path.join(resolvePreferredOpenClawTmpDir(), "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 chmod(rootDir, 0o700).catch(() => undefined);
await assertMatrixQaPrivatePathMode(rootDir, "Matrix QA CLI temp directory");
await mkdir(artifactDir, { mode: 0o700, recursive: true });
await chmod(artifactDir, 0o700).catch(() => undefined);
await assertMatrixQaPrivatePathMode(artifactDir, "Matrix QA CLI artifact directory");
await mkdir(stateDir, { mode: 0o700, recursive: true });
await chmod(stateDir, 0o700).catch(() => undefined);
await assertMatrixQaPrivatePathMode(stateDir, "Matrix QA CLI state directory");
await writeFile(
configPath,
`${JSON.stringify(
{
plugins: {
allow: ["matrix"],
entries: {
matrix: { enabled: true },
},
},
channels: {
matrix: {
defaultAccount: params.accountId,
accounts: {
[params.accountId]: {
accessToken: params.accessToken,
deviceId: params.deviceId,
encryption: true,
homeserver: params.context.baseUrl,
initialSyncLimit: 0,
name: "Matrix QA CLI self-verification",
network: {
dangerouslyAllowPrivateNetwork: true,
},
startupVerification: "off",
userId: params.userId,
},
},
},
},
},
null,
2,
)}\n`,
{ flag: "wx", mode: 0o600 },
);
await assertMatrixQaPrivatePathMode(configPath, "Matrix QA CLI config file");
const env = {
...requireMatrixQaCliRuntimeEnv(params.context),
FORCE_COLOR: "0",
NO_COLOR: "1",
OPENCLAW_CONFIG_PATH: configPath,
OPENCLAW_DISABLE_AUTO_UPDATE: "1",
OPENCLAW_STATE_DIR: stateDir,
};
const run = async (args: string[], timeoutMs = params.context.timeoutMs, stdin?: string) =>
await runMatrixQaOpenClawCli({
args,
env,
stdin,
timeoutMs,
});
const start = (args: string[], timeoutMs = params.context.timeoutMs) =>
startMatrixQaOpenClawCli({
args,
env,
timeoutMs,
});
return {
configPath,
dispose: async () => {
await rm(rootDir, { force: true, recursive: true });
},
run,
rootDir: artifactDir,
start,
stateDir,
};
}
export async function createMatrixQaCliE2eeSetupRuntime(params: {
artifactLabel: string;
context: MatrixQaScenarioContext;
initialConfig?: Record<string, unknown>;
}) {
const outputDir = requireMatrixQaE2eeOutputDir(params.context);
const rootDir = await mkdtemp(
path.join(resolvePreferredOpenClawTmpDir(), "openclaw-matrix-e2ee-setup-qa-"),
);
const artifactDir = path.join(
outputDir,
params.artifactLabel,
randomUUID().replaceAll("-", "").slice(0, 12),
);
const stateDir = path.join(rootDir, "state");
const configPath = path.join(rootDir, "config.json");
await chmod(rootDir, 0o700).catch(() => undefined);
await assertMatrixQaPrivatePathMode(rootDir, "Matrix QA CLI temp directory");
await mkdir(artifactDir, { mode: 0o700, recursive: true });
await chmod(artifactDir, 0o700).catch(() => undefined);
await assertMatrixQaPrivatePathMode(artifactDir, "Matrix QA CLI artifact directory");
await mkdir(stateDir, { mode: 0o700, recursive: true });
await chmod(stateDir, 0o700).catch(() => undefined);
await assertMatrixQaPrivatePathMode(stateDir, "Matrix QA CLI state directory");
await writeFile(
configPath,
`${JSON.stringify(params.initialConfig ?? buildMatrixQaEmptyMatrixCliConfig(), null, 2)}\n`,
{ flag: "wx", mode: 0o600 },
);
await assertMatrixQaPrivatePathMode(configPath, "Matrix QA CLI config file");
const env = {
...requireMatrixQaCliRuntimeEnv(params.context),
FORCE_COLOR: "0",
NO_COLOR: "1",
OPENCLAW_CONFIG_PATH: configPath,
OPENCLAW_DISABLE_AUTO_UPDATE: "1",
OPENCLAW_STATE_DIR: stateDir,
};
const run = async (args: string[], timeoutMs = params.context.timeoutMs, stdin?: string) =>
await runMatrixQaOpenClawCli({
args,
env,
stdin,
timeoutMs,
});
const start = (args: string[], timeoutMs = params.context.timeoutMs) =>
startMatrixQaOpenClawCli({
args,
env,
timeoutMs,
});
return {
configPath,
dispose: async () => {
await rm(rootDir, { force: true, recursive: true });
},
run,
rootDir: artifactDir,
start,
stateDir,
};
}
export async function createMatrixQaCliGatewayRuntime(params: {
artifactLabel: string;
context: MatrixQaScenarioContext;
}) {
const outputDir = requireMatrixQaE2eeOutputDir(params.context);
const artifactDir = path.join(
outputDir,
params.artifactLabel,
randomUUID().replaceAll("-", "").slice(0, 12),
);
await mkdir(artifactDir, { mode: 0o700, recursive: true });
await chmod(artifactDir, 0o700).catch(() => undefined);
await assertMatrixQaPrivatePathMode(artifactDir, "Matrix QA CLI artifact directory");
const env = {
...requireMatrixQaCliRuntimeEnv(params.context),
FORCE_COLOR: "0",
NO_COLOR: "1",
OPENCLAW_DISABLE_AUTO_UPDATE: "1",
};
const run = async (args: string[], timeoutMs = params.context.timeoutMs) =>
await runMatrixQaOpenClawCli({
args,
env,
timeoutMs,
});
return {
dispose: async () => undefined,
rootDir: artifactDir,
run,
};
}

View File

@@ -0,0 +1,289 @@
// Qa Matrix plugin module implements shared CLI scenario runtime E2EE behavior.
import { randomUUID } from "node:crypto";
import { chmod, mkdir, stat, writeFile } from "node:fs/promises";
import path from "node:path";
import type { MatrixVerificationSummary } from "@openclaw/matrix/test-api.js";
import { createMatrixQaClient } from "../../substrate/client.js";
import { createMatrixQaE2eeScenarioClient } from "../../substrate/e2ee-client.js";
import type { MatrixQaE2eeScenarioId } from "./scenario-catalog.js";
import {
formatMatrixQaCliCommand,
redactMatrixQaCliOutput,
type MatrixQaCliRunResult,
} from "./scenario-runtime-cli.js";
import {
formatMatrixQaSasEmoji,
requireMatrixQaE2eeOutputDir,
requireMatrixQaRegistrationToken,
} from "./scenario-runtime-e2ee-shared.js";
import type { MatrixQaScenarioContext } from "./scenario-runtime-shared.js";
export type MatrixQaCliVerificationStatus = {
backup?: {
decryptionKeyCached?: boolean | null;
keyLoadError?: string | null;
matchesDecryptionKey?: boolean | null;
trusted?: boolean | null;
};
backupVersion?: string | null;
crossSigningVerified?: boolean;
encryptionEnabled?: boolean;
pendingVerifications?: number;
recoveryKeyStored?: boolean;
serverDeviceKnown?: boolean;
verified?: boolean;
signedByOwner?: boolean;
deviceId?: string | null;
userId?: string | null;
};
export type MatrixQaCliEncryptionSetupStatus = {
accountId?: string;
bootstrap?: {
error?: string;
success?: boolean;
};
configPath?: string;
encryptionChanged?: boolean;
status?: MatrixQaCliVerificationStatus;
success?: boolean;
};
export type MatrixQaCliAccountAddStatus = {
accountId?: string;
configPath?: string;
encryptionEnabled?: boolean;
verificationBootstrap?: {
attempted?: boolean;
backupVersion?: string | null;
error?: string;
success?: boolean;
};
};
export type MatrixQaCliBackupRestoreStatus = {
success?: boolean;
backup?: MatrixQaCliVerificationStatus["backup"];
error?: string;
};
export function isMatrixQaCliBackupUsable(
backup: MatrixQaCliVerificationStatus["backup"],
opts: { allowUntrustedMatchingKey?: boolean } = {},
): boolean {
return Boolean(
(backup?.trusted || opts.allowUntrustedMatchingKey === true) &&
backup?.matchesDecryptionKey &&
backup.decryptionKeyCached &&
!backup.keyLoadError,
);
}
function parseMatrixQaCliJsonText(text: string): unknown {
const candidate = text.trim();
if (!candidate) {
throw new Error("no JSON payload found");
}
return JSON.parse(candidate) as unknown;
}
export function parseMatrixQaCliJson(result: MatrixQaCliRunResult): unknown {
const stdout = result.stdout.trim();
const stderr = result.stderr.trim();
if (stdout) {
try {
return parseMatrixQaCliJsonText(stdout);
} catch (error) {
throw new Error(
`${formatMatrixQaCliCommand(result.args)} printed invalid JSON: ${
error instanceof Error ? error.message : String(error)
}\nstdout:\n${redactMatrixQaCliOutput(stdout)}`,
{ cause: error },
);
}
}
if (!stderr) {
throw new Error(`${formatMatrixQaCliCommand(result.args)} did not print JSON`);
}
try {
return parseMatrixQaCliJsonText(stderr);
} catch (error) {
throw new Error(
`${formatMatrixQaCliCommand(result.args)} printed invalid JSON: ${
error instanceof Error ? error.message : String(error)
}\nstderr:\n${redactMatrixQaCliOutput(stderr)}`,
{ cause: error },
);
}
}
export function buildMatrixQaPluginActivationConfig() {
return {
plugins: {
allow: ["matrix"],
entries: {
matrix: { enabled: true },
},
},
};
}
export function buildMatrixQaEmptyMatrixCliConfig() {
return {
...buildMatrixQaPluginActivationConfig(),
channels: {
matrix: {
enabled: true,
accounts: {},
},
},
};
}
export async function registerMatrixQaCliE2eeAccount(params: {
context: MatrixQaScenarioContext;
deviceName: string;
scenarioId: MatrixQaE2eeScenarioId;
}) {
const localpartSuffix = params.scenarioId
.replace(/^matrix-e2ee-cli-/, "")
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "")
.slice(0, 24);
const account = await createMatrixQaClient({
baseUrl: params.context.baseUrl,
}).registerWithToken({
deviceName: params.deviceName,
localpart: `qa-cli-${localpartSuffix}-${randomUUID().replaceAll("-", "").slice(0, 8)}`,
password: `matrix-qa-${randomUUID()}`,
registrationToken: requireMatrixQaRegistrationToken(params.context),
});
if (!account.deviceId) {
throw new Error(
`Matrix CLI QA registration for ${params.scenarioId} did not return a device id`,
);
}
return account;
}
export async function createMatrixQaE2eeCliOwnerClient(params: {
account: Awaited<ReturnType<typeof registerMatrixQaCliE2eeAccount>>;
context: MatrixQaScenarioContext;
scenarioId: MatrixQaE2eeScenarioId;
}) {
return await createMatrixQaE2eeScenarioClient({
accessToken: params.account.accessToken,
actorId: `cli-owner-${randomUUID().slice(0, 8)}`,
baseUrl: params.context.baseUrl,
deviceId: params.account.deviceId,
observedEvents: params.context.observedEvents,
outputDir: requireMatrixQaE2eeOutputDir(params.context),
password: params.account.password,
scenarioId: params.scenarioId,
timeoutMs: params.context.timeoutMs,
userId: params.account.userId,
});
}
export function parseMatrixQaCliSasText(
text: string,
label: string,
): { kind: "emoji"; value: string } | { kind: "decimal"; value: string } {
const emoji = text.match(/^SAS emoji:\s*(.+)$/m)?.[1]?.trim();
if (emoji) {
return { kind: "emoji", value: emoji };
}
const decimal = text.match(/^SAS decimals:\s*(.+)$/m)?.[1]?.trim();
if (decimal) {
return { kind: "decimal", value: decimal };
}
throw new Error(`${label} did not print SAS emoji or decimals`);
}
export function parseMatrixQaCliSummaryField(text: string, field: string): string | null {
const escaped = field.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
return text.match(new RegExp(`^${escaped}:\\s*(.+)$`, "m"))?.[1]?.trim() ?? null;
}
export async function writeMatrixQaCliOutputArtifacts(params: {
label: string;
result: MatrixQaCliRunResult;
rootDir: string;
}) {
await mkdir(params.rootDir, { mode: 0o700, recursive: true });
await chmod(params.rootDir, 0o700).catch(() => undefined);
const prefix = params.label.replace(/[^A-Za-z0-9_-]/g, "-");
const stdoutPath = path.join(params.rootDir, `${prefix}.stdout.txt`);
const stderrPath = path.join(params.rootDir, `${prefix}.stderr.txt`);
await Promise.all([
writeFile(stdoutPath, redactMatrixQaCliOutput(params.result.stdout), { mode: 0o600 }),
writeFile(stderrPath, redactMatrixQaCliOutput(params.result.stderr), { mode: 0o600 }),
]);
return { stderrPath, stdoutPath };
}
export async function assertMatrixQaPrivatePathMode(pathToCheck: string, label: string) {
if (process.platform === "win32") {
return;
}
const mode = (await stat(pathToCheck)).mode & 0o777;
if ((mode & 0o077) !== 0) {
throw new Error(`${label} permissions are too broad: ${mode.toString(8)}`);
}
}
export function assertMatrixQaCliSasMatches(params: {
cliSas: ReturnType<typeof parseMatrixQaCliSasText>;
owner: MatrixVerificationSummary;
}) {
if (params.cliSas.kind === "emoji") {
const ownerEmoji = formatMatrixQaSasEmoji(params.owner).join(" | ");
if (!ownerEmoji) {
throw new Error("Matrix owner client did not expose SAS emoji");
}
if (params.cliSas.value !== ownerEmoji) {
throw new Error("Matrix CLI SAS emoji did not match the owner client");
}
return ownerEmoji.split(" | ");
}
const ownerDecimal = params.owner.sas?.decimal?.join(" ");
if (!ownerDecimal) {
throw new Error("Matrix owner client did not expose SAS decimals");
}
if (params.cliSas.value !== ownerDecimal) {
throw new Error("Matrix CLI SAS decimals did not match the owner client");
}
return [ownerDecimal];
}
export function isMatrixQaCliOwnerSelfVerification(params: {
cliDeviceId?: string;
ownerUserId: string;
requireCompleted?: boolean;
requirePending?: boolean;
requireSas?: boolean;
summary: MatrixVerificationSummary;
transactionId?: string;
}) {
const summary = params.summary;
if (
!summary.isSelfVerification ||
summary.initiatedByMe ||
summary.otherUserId !== params.ownerUserId
) {
return false;
}
if (params.transactionId) {
if (summary.transactionId !== params.transactionId) {
return false;
}
} else if (params.cliDeviceId && summary.otherDeviceId !== params.cliDeviceId) {
return false;
}
if (params.requirePending === true && !summary.pending) {
return false;
}
if (params.requireSas === true && !summary.hasSas) {
return false;
}
return params.requireCompleted !== true || summary.completed;
}

View File

@@ -0,0 +1,269 @@
// Qa Matrix plugin module implements self-verification CLI E2EE scenarios.
import { createMatrixQaClient } from "../../substrate/client.js";
import { createMatrixQaCliSelfVerificationRuntime } from "./scenario-runtime-e2ee-cli-runtime.js";
import {
assertMatrixQaCliSasMatches,
createMatrixQaE2eeCliOwnerClient,
isMatrixQaCliBackupUsable,
isMatrixQaCliOwnerSelfVerification,
parseMatrixQaCliJson,
parseMatrixQaCliSasText,
parseMatrixQaCliSummaryField,
registerMatrixQaCliE2eeAccount,
type MatrixQaCliBackupRestoreStatus,
type MatrixQaCliVerificationStatus,
writeMatrixQaCliOutputArtifacts,
} from "./scenario-runtime-e2ee-cli-shared.js";
import {
ensureMatrixQaE2eeOwnDeviceVerified,
waitForMatrixQaVerificationSummary,
} from "./scenario-runtime-e2ee-shared.js";
import type { MatrixQaScenarioContext } from "./scenario-runtime-shared.js";
import type { MatrixQaScenarioExecution } from "./scenario-types.js";
export async function runMatrixQaE2eeCliSelfVerificationScenario(
context: MatrixQaScenarioContext,
): Promise<MatrixQaScenarioExecution> {
const accountId = "cli";
const account = await registerMatrixQaCliE2eeAccount({
context,
deviceName: "OpenClaw Matrix QA CLI Self Verification Owner",
scenarioId: "matrix-e2ee-cli-self-verification",
});
const owner = await createMatrixQaE2eeCliOwnerClient({
account,
context,
scenarioId: "matrix-e2ee-cli-self-verification",
});
try {
const ownerReady = await ensureMatrixQaE2eeOwnDeviceVerified({
client: owner,
label: "CLI self-verification owner",
});
const encodedRecoveryKey = ownerReady.recoveryKey?.encodedPrivateKey?.trim();
if (!encodedRecoveryKey) {
throw new Error("Matrix E2EE self-verification scenario did not expose a recovery key");
}
const loginClient = createMatrixQaClient({
baseUrl: context.baseUrl,
});
const cliDevice = await loginClient.loginWithPassword({
deviceName: "OpenClaw Matrix QA CLI Self Verification Device",
password: account.password,
userId: account.userId,
});
if (!cliDevice.deviceId) {
throw new Error("Matrix E2EE CLI verification login did not return a device id");
}
const cli = await createMatrixQaCliSelfVerificationRuntime({
accountId,
accessToken: cliDevice.accessToken,
context,
deviceId: cliDevice.deviceId,
userId: cliDevice.userId,
});
try {
const restoreResult = await cli.run(
[
"matrix",
"verify",
"backup",
"restore",
"--account",
accountId,
"--recovery-key-stdin",
"--json",
],
context.timeoutMs,
`${encodedRecoveryKey}\n`,
);
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,
"--timeout-ms",
String(context.timeoutMs),
],
context.timeoutMs * 2,
);
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,
ownerUserId: account.userId,
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,
ownerUserId: account.userId,
requireSas: true,
summary,
transactionId: cliTransactionId ?? undefined,
}),
timeoutMs: context.timeoutMs,
});
const sasArtifact = assertMatrixQaCliSasMatches({
cliSas,
owner: ownerSas,
});
const ownerConfirm = owner.confirmVerificationSas(ownerSas.id);
await session.writeStdin("yes\n");
session.endStdin();
await ownerConfirm;
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,
ownerUserId: account.userId,
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 {
try {
await cli.dispose();
} finally {
await owner.stop().catch(() => undefined);
await owner.deleteOwnDevices([cliDevice.deviceId]).catch(() => undefined);
}
}
} finally {
await owner.stop().catch(() => undefined);
}
}

View File

@@ -0,0 +1,439 @@
// Qa Matrix plugin module implements message scenario runtime E2EE behavior.
import { randomUUID } from "node:crypto";
import { startMatrixQaFaultProxy } from "../../substrate/fault-proxy.js";
import {
buildMatrixQaImageUnderstandingPrompt,
createMatrixQaSplitColorImagePng,
hasMatrixQaExpectedColorReply,
MATRIX_QA_IMAGE_ATTACHMENT_FILENAME,
} from "./scenario-media-fixtures.js";
import {
patchMatrixQaGatewayMatrixAccount,
readMatrixQaGatewayMatrixAccount,
replaceMatrixQaGatewayMatrixAccount,
} from "./scenario-runtime-config.js";
import {
buildMatrixE2eeReplyArtifact,
buildSyncStateAfterMissingEncryptionFaultRule,
runMatrixQaE2eeTopLevelScenario,
runMatrixQaE2eeTopLevelWithClient,
withMatrixQaE2eeDriver,
withMatrixQaIsolatedE2eeDriverRoom,
} from "./scenario-runtime-e2ee-room.js";
import {
MATRIX_QA_SYNC_STATE_AFTER_FAULT_RULE_ID,
MATRIX_QA_SYNC_STATE_AFTER_PARAM,
isMatrixQaE2eeNoticeTriggeredSutReply,
requireMatrixQaGatewayConfigPath,
resolveMatrixQaE2eeScenarioGroupRoom,
} from "./scenario-runtime-e2ee-shared.js";
import {
assertThreadReplyArtifact,
buildMatrixQaToken,
buildMatrixReplyDetails,
buildMentionPrompt,
isMatrixQaExactMarkerReply,
resolveMatrixQaNoReplyWindowMs,
type MatrixQaScenarioContext,
} from "./scenario-runtime-shared.js";
import type { MatrixQaReplyArtifact, MatrixQaScenarioExecution } from "./scenario-types.js";
export async function runMatrixQaE2eeBasicReplyScenario(
context: MatrixQaScenarioContext,
): Promise<MatrixQaScenarioExecution> {
const result = await runMatrixQaE2eeTopLevelScenario(context, {
scenarioId: "matrix-e2ee-basic-reply",
tokenPrefix: "MATRIX_QA_E2EE_BASIC",
});
return {
artifacts: {
driverEventId: result.driverEventId,
reply: result.reply,
roomKey: result.roomKey,
roomId: result.roomId,
},
details: [
`encrypted room key: ${result.roomKey}`,
`encrypted room id: ${result.roomId}`,
`driver event: ${result.driverEventId}`,
...buildMatrixReplyDetails("E2EE reply", result.reply),
].join("\n"),
};
}
export async function runMatrixQaE2eeStateAfterMissingEncryptionScenario(
context: MatrixQaScenarioContext,
): Promise<MatrixQaScenarioExecution> {
if (!context.restartGatewayAfterStateMutation) {
throw new Error("Matrix E2EE state_after QA scenario requires hard gateway restart support");
}
const accountId = context.sutAccountId ?? "sut";
const configPath = requireMatrixQaGatewayConfigPath(context);
const originalAccountConfig = await readMatrixQaGatewayMatrixAccount({
accountId,
configPath,
});
const proxy = await startMatrixQaFaultProxy({
targetBaseUrl: context.faultProxyTargetBaseUrl ?? context.baseUrl,
...context.faultProxyObserver,
rules: [buildSyncStateAfterMissingEncryptionFaultRule(context.sutAccessToken)],
});
let gatewayPatched = false;
try {
await context.restartGatewayAfterStateMutation(
async () => {
await patchMatrixQaGatewayMatrixAccount({
accountId,
accountPatch: {
homeserver: proxy.baseUrl,
network: {
dangerouslyAllowPrivateNetwork: true,
},
},
configPath,
});
gatewayPatched = true;
},
{
timeoutMs: context.timeoutMs,
waitAccountId: accountId,
},
);
const result = await runMatrixQaE2eeTopLevelScenario(context, {
scenarioId: "matrix-e2ee-state-after-missing-encryption",
tokenPrefix: "MATRIX_QA_E2EE_STATE_AFTER",
});
const stateAfterHits = proxy
.hits()
.filter((hit) => hit.ruleId === MATRIX_QA_SYNC_STATE_AFTER_FAULT_RULE_ID);
if (stateAfterHits.length > 0) {
throw new Error(
`Matrix E2EE gateway still sent ${MATRIX_QA_SYNC_STATE_AFTER_PARAM}=true on /sync`,
);
}
return {
artifacts: {
driverEventId: result.driverEventId,
faultProxyBaseUrl: proxy.baseUrl,
reply: result.reply,
roomKey: result.roomKey,
roomId: result.roomId,
stateAfterFaultHitCount: stateAfterHits.length,
stateAfterFaultRuleId: MATRIX_QA_SYNC_STATE_AFTER_FAULT_RULE_ID,
strippedSyncStateAfterParam: true,
},
details: [
`encrypted room key: ${result.roomKey}`,
`encrypted room id: ${result.roomId}`,
`driver event: ${result.driverEventId}`,
`fault proxy: ${proxy.baseUrl}`,
`state_after sync opt-in hits: ${stateAfterHits.length}`,
...buildMatrixReplyDetails("E2EE state_after reply", result.reply),
].join("\n"),
};
} finally {
if (gatewayPatched) {
await context
.restartGatewayAfterStateMutation(
async () => {
await replaceMatrixQaGatewayMatrixAccount({
accountConfig: originalAccountConfig,
accountId,
configPath,
});
},
{
timeoutMs: context.timeoutMs,
waitAccountId: accountId,
},
)
.catch(() => undefined);
}
await proxy.stop().catch(() => undefined);
}
}
export async function runMatrixQaE2eeThreadFollowUpScenario(
context: MatrixQaScenarioContext,
): Promise<MatrixQaScenarioExecution> {
const { roomId, roomKey } = resolveMatrixQaE2eeScenarioGroupRoom(
context,
"matrix-e2ee-thread-follow-up",
);
const result = await withMatrixQaE2eeDriver(
context,
"matrix-e2ee-thread-follow-up",
async (client) => {
await client.prime();
const rootEventId = await client.sendTextMessage({
body: `E2EE thread root ${randomUUID().slice(0, 8)}`,
roomId,
});
const token = buildMatrixQaToken("MATRIX_QA_E2EE_THREAD");
const driverEventId = await client.sendTextMessage({
body: buildMentionPrompt(context.sutUserId, token),
mentionUserIds: [context.sutUserId],
replyToEventId: rootEventId,
roomId,
threadRootEventId: rootEventId,
});
const matched = await client.waitForRoomEvent({
predicate: (event) =>
isMatrixQaExactMarkerReply(event, {
roomId,
sutUserId: context.sutUserId,
token,
}) &&
event.relatesTo?.relType === "m.thread" &&
event.relatesTo.eventId === rootEventId,
roomId,
timeoutMs: context.timeoutMs,
});
const reply = buildMatrixE2eeReplyArtifact(matched.event, token);
assertThreadReplyArtifact(reply, {
expectedRootEventId: rootEventId,
label: "E2EE threaded reply",
});
return {
driverEventId,
reply,
rootEventId,
token,
};
},
);
return {
artifacts: {
driverEventId: result.driverEventId,
reply: result.reply,
rootEventId: result.rootEventId,
roomKey,
roomId,
},
details: [
`encrypted room key: ${roomKey}`,
`encrypted room id: ${roomId}`,
`thread root event: ${result.rootEventId}`,
`mention trigger event: ${result.driverEventId}`,
...buildMatrixReplyDetails("E2EE threaded reply", result.reply),
].join("\n"),
};
}
export async function runMatrixQaE2eeRestartResumeScenario(
context: MatrixQaScenarioContext,
): Promise<MatrixQaScenarioExecution> {
if (!context.restartGateway) {
throw new Error("Matrix E2EE restart scenario requires gateway restart support");
}
const restartGateway = context.restartGateway;
return await withMatrixQaIsolatedE2eeDriverRoom(
context,
"matrix-e2ee-restart-resume",
async ({ client, driverUserId, roomId, roomKey }) => {
const first = await runMatrixQaE2eeTopLevelWithClient(context, {
client,
driverUserId,
roomId,
roomKey,
tokenPrefix: "MATRIX_QA_E2EE_BEFORE_RESTART",
});
await restartGateway();
const recovered = await runMatrixQaE2eeTopLevelWithClient(context, {
client,
driverUserId,
roomId,
roomKey,
tokenPrefix: "MATRIX_QA_E2EE_AFTER_RESTART",
});
return {
artifacts: {
driverUserId,
firstDriverEventId: first.driverEventId,
firstReply: first.reply,
recoveredDriverEventId: recovered.driverEventId,
recoveredReply: recovered.reply,
restartSignal: "gateway-restart",
roomKey: recovered.roomKey,
roomId: recovered.roomId,
},
details: [
`encrypted room key: ${recovered.roomKey}`,
`encrypted room id: ${recovered.roomId}`,
`isolated driver user: ${driverUserId}`,
`pre-restart event: ${first.driverEventId}`,
...buildMatrixReplyDetails("pre-restart reply", first.reply),
`post-restart event: ${recovered.driverEventId}`,
...buildMatrixReplyDetails("post-restart reply", recovered.reply),
].join("\n"),
};
},
);
}
export async function runMatrixQaE2eeVerificationNoticeNoTriggerScenario(
context: MatrixQaScenarioContext,
): Promise<MatrixQaScenarioExecution> {
const { roomId, roomKey } = resolveMatrixQaE2eeScenarioGroupRoom(
context,
"matrix-e2ee-verification-notice-no-trigger",
);
return await withMatrixQaE2eeDriver(
context,
"matrix-e2ee-verification-notice-no-trigger",
async (client) => {
await client.prime();
const token = buildMatrixQaToken("MATRIX_QA_E2EE_VERIFY_NOTICE");
const body = `Matrix verification started with ${context.driverUserId}; ${buildMentionPrompt(
context.sutUserId,
token,
)}`;
const noticeSentAt = Date.now();
const noticeEventId = await client.sendNoticeMessage({
body,
mentionUserIds: [context.sutUserId],
roomId,
});
const result = await client.waitForOptionalRoomEvent({
predicate: (event) =>
isMatrixQaE2eeNoticeTriggeredSutReply({
event,
noticeEventId,
noticeSentAt,
roomId,
sutUserId: context.sutUserId,
token,
}),
roomId,
timeoutMs: resolveMatrixQaNoReplyWindowMs(context.timeoutMs),
});
if (result.matched) {
throw new Error(`unexpected E2EE verification-notice reply: ${result.event.eventId}`);
}
return {
artifacts: {
expectedNoReplyWindowMs: resolveMatrixQaNoReplyWindowMs(context.timeoutMs),
noticeEventId,
roomKey,
roomId,
},
details: [
`encrypted room key: ${roomKey}`,
`encrypted room id: ${roomId}`,
`verification notice event: ${noticeEventId}`,
`waited ${resolveMatrixQaNoReplyWindowMs(context.timeoutMs)}ms with no SUT reply`,
].join("\n"),
};
},
);
}
export async function runMatrixQaE2eeArtifactRedactionScenario(
context: MatrixQaScenarioContext,
): Promise<MatrixQaScenarioExecution> {
return await withMatrixQaIsolatedE2eeDriverRoom(
context,
"matrix-e2ee-artifact-redaction",
async ({ client, driverUserId, roomId, roomKey }) => {
const result = await runMatrixQaE2eeTopLevelWithClient(context, {
client,
driverUserId,
roomId,
roomKey,
tokenPrefix: "MATRIX_QA_E2EE_REDACT",
});
const leaked = context.observedEvents.some(
(event) =>
event.roomId === result.roomId &&
(event.body?.includes(result.token) || event.formattedBody?.includes(result.token)),
);
if (!leaked) {
throw new Error(
"Matrix E2EE redaction scenario did not observe decrypted content in memory",
);
}
return {
artifacts: {
driverEventId: result.driverEventId,
driverUserId,
reply: result.reply,
roomKey: result.roomKey,
roomId: result.roomId,
},
details: [
"decrypted E2EE payload reached in-memory assertions only",
"observed-event artifacts redact body/formatted_body unless OPENCLAW_QA_MATRIX_CAPTURE_CONTENT=1",
`encrypted room id: ${result.roomId}`,
`isolated driver user: ${driverUserId}`,
...buildMatrixReplyDetails("E2EE reply", result.reply),
].join("\n"),
};
},
);
}
export async function runMatrixQaE2eeMediaImageScenario(
context: MatrixQaScenarioContext,
): Promise<MatrixQaScenarioExecution> {
return await withMatrixQaIsolatedE2eeDriverRoom(
context,
"matrix-e2ee-media-image",
async ({ client, driverUserId, roomId, roomKey }) => {
const startSince = await client.prime();
const triggerBody = buildMatrixQaImageUnderstandingPrompt(context.sutUserId);
const driverEventId = await client.sendImageMessage({
body: triggerBody,
buffer: createMatrixQaSplitColorImagePng(),
contentType: "image/png",
fileName: MATRIX_QA_IMAGE_ATTACHMENT_FILENAME,
mentionUserIds: [context.sutUserId],
roomId,
});
const attachmentEvent = await client.waitForRoomEvent({
predicate: (event) =>
event.roomId === roomId &&
event.eventId === driverEventId &&
event.sender === driverUserId &&
event.attachment?.kind === "image" &&
event.attachment.caption === triggerBody,
roomId,
timeoutMs: context.timeoutMs,
});
const matched = await client.waitForRoomEvent({
predicate: (event) =>
event.roomId === roomId &&
event.sender === context.sutUserId &&
event.type === "m.room.message" &&
event.relatesTo === undefined &&
hasMatrixQaExpectedColorReply(event.body),
roomId,
timeoutMs: context.timeoutMs,
});
const reply: MatrixQaReplyArtifact = {
eventId: matched.event.eventId,
mentions: matched.event.mentions,
relatesTo: matched.event.relatesTo,
sender: matched.event.sender,
};
return {
artifacts: {
attachmentFilename: MATRIX_QA_IMAGE_ATTACHMENT_FILENAME,
driverEventId,
driverUserId,
reply,
roomKey,
roomId,
},
details: [
`encrypted room key: ${roomKey}`,
`encrypted room id: ${roomId}`,
`isolated driver user: ${driverUserId}`,
`driver encrypted image event: ${driverEventId}`,
`driver encrypted image filename: ${MATRIX_QA_IMAGE_ATTACHMENT_FILENAME}`,
`driver encrypted image since: ${attachmentEvent.since ?? startSince ?? "<none>"}`,
...buildMatrixReplyDetails("E2EE image reply", reply),
].join("\n"),
};
},
);
}

View File

@@ -0,0 +1,285 @@
// Qa Matrix plugin module implements recovery scenario runtime E2EE behavior.
import { randomUUID } from "node:crypto";
import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
import { createMatrixQaClient } from "../../substrate/client.js";
import { createMatrixQaE2eeScenarioClient } from "../../substrate/e2ee-client.js";
import {
assertMatrixQaExpectedBootstrapFailure,
assertMatrixQaFaultedRecoveryOwnerVerificationRequired,
runMatrixQaFaultedE2eeBootstrap,
runMatrixQaFaultedRecoveryOwnerVerification,
withMatrixQaE2eeDriver,
} from "./scenario-runtime-e2ee-room.js";
import {
assertMatrixQaBootstrapSucceeded,
ensureMatrixQaE2eeOwnDeviceVerified,
MATRIX_QA_OWNER_SIGNATURE_UPLOAD_BLOCKED_RULE_ID,
MATRIX_QA_ROOM_KEY_BACKUP_VERSION_ENDPOINT,
requireMatrixQaE2eeOutputDir,
requireMatrixQaPassword,
resolveMatrixQaE2eeScenarioGroupRoom,
waitForMatrixQaNonEmptyRoomKeyRestore,
} from "./scenario-runtime-e2ee-shared.js";
import type { MatrixQaScenarioContext } from "./scenario-runtime-shared.js";
import type { MatrixQaScenarioExecution } from "./scenario-types.js";
export async function runMatrixQaE2eeBootstrapSuccessScenario(
context: MatrixQaScenarioContext,
): Promise<MatrixQaScenarioExecution> {
requireMatrixQaPassword(context, "driver");
return await withMatrixQaE2eeDriver(context, "matrix-e2ee-bootstrap-success", async (client) => {
const initial = await client.bootstrapOwnDeviceVerification();
assertMatrixQaBootstrapSucceeded("driver initial", initial);
const result = await client.bootstrapOwnDeviceVerification({
forceResetCrossSigning: true,
});
assertMatrixQaBootstrapSucceeded("driver", result);
return {
artifacts: {
backupCreatedVersion: result.verification.backupVersion,
bootstrapActor: "driver",
bootstrapSuccess: true,
currentDeviceId: result.verification.deviceId,
recoveryKeyId: result.verification.recoveryKeyId,
recoveryKeyStored: result.verification.recoveryKeyStored,
},
details: [
"driver bootstrap and guarded cross-signing reset succeeded through real Matrix crypto bootstrap",
`device verified: ${result.verification.verified ? "yes" : "no"}`,
`cross-signing verified: ${result.verification.crossSigningVerified ? "yes" : "no"}`,
`signed by owner: ${result.verification.signedByOwner ? "yes" : "no"}`,
`cross-signing published: ${result.crossSigning.published ? "yes" : "no"}`,
`room-key backup version: ${result.verification.backupVersion ?? "<none>"}`,
`recovery key id: ${result.verification.recoveryKeyId ?? "<none>"}`,
].join("\n"),
};
});
}
export async function runMatrixQaE2eeRecoveryKeyLifecycleScenario(
context: MatrixQaScenarioContext,
): Promise<MatrixQaScenarioExecution> {
const driverPassword = requireMatrixQaPassword(context, "driver");
return await withMatrixQaE2eeDriver(
context,
"matrix-e2ee-recovery-key-lifecycle",
async (client) => {
const { roomId } = resolveMatrixQaE2eeScenarioGroupRoom(
context,
"matrix-e2ee-recovery-key-lifecycle",
);
const ready = await ensureMatrixQaE2eeOwnDeviceVerified({
client,
label: "driver",
});
const recoveryKey = ready.recoveryKey;
const encodedRecoveryKey = recoveryKey?.encodedPrivateKey?.trim();
if (!encodedRecoveryKey) {
throw new Error("Matrix E2EE bootstrap did not expose an encoded recovery key");
}
const seededEventId = await client.sendTextMessage({
body: `E2EE recovery-key restore seed ${randomUUID().slice(0, 8)}`,
roomId,
});
const loginClient = createMatrixQaClient({
baseUrl: context.baseUrl,
});
const recoveryDevice = await loginClient.loginWithPassword({
deviceName: "OpenClaw Matrix QA Recovery Restore Device",
password: driverPassword,
userId: context.driverUserId,
});
if (!recoveryDevice.deviceId) {
throw new Error("Matrix E2EE recovery login did not return a secondary device id");
}
const recoveryClient = await createMatrixQaE2eeScenarioClient({
accessToken: recoveryDevice.accessToken,
actorId: `driver-recovery-${randomUUID().slice(0, 8)}`,
baseUrl: context.baseUrl,
deviceId: recoveryDevice.deviceId,
observedEvents: context.observedEvents,
outputDir: requireMatrixQaE2eeOutputDir(context),
password: recoveryDevice.password,
scenarioId: "matrix-e2ee-recovery-key-lifecycle",
timeoutMs: context.timeoutMs,
userId: recoveryDevice.userId,
});
let cleanupRecoveryDevice = true;
try {
const recoveryVerification = await recoveryClient.verifyWithRecoveryKey(encodedRecoveryKey);
if (!recoveryVerification.success) {
throw new Error(
`Matrix E2EE recovery device verification failed: ${recoveryVerification.error ?? "unknown error"}`,
);
}
const restored = await waitForMatrixQaNonEmptyRoomKeyRestore({
client: recoveryClient,
recoveryKey: encodedRecoveryKey,
timeoutMs: context.timeoutMs,
});
const reset = await recoveryClient.resetRoomKeyBackup();
if (!reset.success) {
throw new Error(
`Matrix E2EE room-key backup reset failed: ${reset.error ?? "unknown error"}`,
);
}
const resetRecoveryKey = await recoveryClient.getRecoveryKey();
const resetEncodedRecoveryKey = resetRecoveryKey?.encodedPrivateKey?.trim();
if (resetEncodedRecoveryKey && resetEncodedRecoveryKey !== encodedRecoveryKey) {
const ownerRecovery = await client.verifyWithRecoveryKey(resetEncodedRecoveryKey);
if (!ownerRecovery.success) {
throw new Error(
`Matrix E2EE owner could not refresh recovery key after backup reset: ${
ownerRecovery.error ?? "unknown error"
}`,
);
}
}
await recoveryClient.stop();
await client.stop().catch(() => undefined);
await client.deleteOwnDevices([recoveryDevice.deviceId]).catch(() => undefined);
cleanupRecoveryDevice = false;
return {
artifacts: {
backupCreatedVersion: reset.createdVersion,
backupReset: reset.success,
backupRestored: restored.success,
bootstrapActor: "driver",
bootstrapSuccess: ready.bootstrap?.success ?? true,
recoveryDeviceId: recoveryDevice.deviceId,
recoveryKeyId: recoveryKey?.keyId ?? null,
recoveryKeyUsable:
recoveryVerification.recoveryKeyAccepted && recoveryVerification.backupUsable,
recoveryKeyStored: true,
recoveryVerified: recoveryVerification.deviceOwnerVerified,
restoreImported: restored.imported,
restoreTotal: restored.total,
seededEventId,
},
details: [
"driver recovery lifecycle completed through real Matrix recovery APIs",
`bootstrap backup version: ${ready.verification.backupVersion ?? "<none>"}`,
`seeded encrypted event: ${seededEventId}`,
`recovery device: ${recoveryDevice.deviceId}`,
`recovery key usable: ${recoveryVerification.backupUsable ? "yes" : "no"}`,
`recovery device verified: ${recoveryVerification.deviceOwnerVerified ? "yes" : "no"}`,
`restore imported/total: ${restored.imported}/${restored.total}`,
`restore loaded from secret storage: ${restored.loadedFromSecretStorage ? "yes" : "no"}`,
`reset previous version: ${reset.previousVersion ?? "<none>"}`,
`reset created version: ${reset.createdVersion ?? "<none>"}`,
].join("\n"),
};
} finally {
if (cleanupRecoveryDevice) {
await recoveryClient.stop().catch(() => undefined);
await client.stop().catch(() => undefined);
await client.deleteOwnDevices([recoveryDevice.deviceId]).catch(() => undefined);
}
}
},
);
}
export async function runMatrixQaE2eeRecoveryOwnerVerificationRequiredScenario(
context: MatrixQaScenarioContext,
): Promise<MatrixQaScenarioExecution> {
const driverPassword = requireMatrixQaPassword(context, "driver");
return await withMatrixQaE2eeDriver(
context,
"matrix-e2ee-recovery-owner-verification-required",
async (client) => {
const { roomId } = resolveMatrixQaE2eeScenarioGroupRoom(
context,
"matrix-e2ee-recovery-owner-verification-required",
);
const ready = await ensureMatrixQaE2eeOwnDeviceVerified({
client,
label: "driver",
});
const recoveryKey = ready.recoveryKey;
const encodedRecoveryKey = recoveryKey?.encodedPrivateKey?.trim();
if (!encodedRecoveryKey) {
throw new Error("Matrix E2EE bootstrap did not expose an encoded recovery key");
}
const seededEventId = await client.sendTextMessage({
body: `E2EE recovery owner-verification seed ${randomUUID().slice(0, 8)}`,
roomId,
});
const loginClient = createMatrixQaClient({
baseUrl: context.baseUrl,
});
const recoveryDevice = await loginClient.loginWithPassword({
deviceName: "OpenClaw Matrix QA Owner Verification Required Device",
password: driverPassword,
userId: context.driverUserId,
});
if (!recoveryDevice.deviceId) {
throw new Error("Matrix E2EE recovery login did not return a secondary device id");
}
try {
const faulted = await runMatrixQaFaultedRecoveryOwnerVerification({
accessToken: recoveryDevice.accessToken,
context,
deviceId: recoveryDevice.deviceId,
encodedRecoveryKey,
userId: recoveryDevice.userId,
});
assertMatrixQaFaultedRecoveryOwnerVerificationRequired(faulted);
return {
artifacts: {
backupRestored: faulted.restore.success,
backupUsable: faulted.verification.backupUsable,
faultHitCount: faulted.faultHits.length,
faultedEndpoints: faulted.faultHits.map((hit) => hit.path),
faultRuleId: MATRIX_QA_OWNER_SIGNATURE_UPLOAD_BLOCKED_RULE_ID,
recoveryDeviceId: recoveryDevice.deviceId,
recoveryKeyAccepted: faulted.verification.recoveryKeyAccepted,
recoveryKeyId: recoveryKey?.keyId ?? null,
recoveryVerified: faulted.verification.deviceOwnerVerified,
restoreImported: faulted.restore.imported,
restoreTotal: faulted.restore.total,
verificationSuccess: faulted.verification.success,
},
details: [
"driver recovery key unlocked backup while owner signature upload was blocked",
`seeded encrypted event: ${seededEventId}`,
`recovery device: ${recoveryDevice.deviceId}`,
`fault hits: ${faulted.faultHits.length}`,
`recovery key accepted: ${faulted.verification.recoveryKeyAccepted ? "yes" : "no"}`,
`backup usable: ${faulted.verification.backupUsable ? "yes" : "no"}`,
`device owner verified: ${faulted.verification.deviceOwnerVerified ? "yes" : "no"}`,
`restore imported/total: ${faulted.restore.imported}/${faulted.restore.total}`,
].join("\n"),
};
} finally {
await client.stop().catch(() => undefined);
await client.deleteOwnDevices([recoveryDevice.deviceId]).catch(() => undefined);
}
},
);
}
export async function runMatrixQaE2eeKeyBootstrapFailureScenario(
context: MatrixQaScenarioContext,
): Promise<MatrixQaScenarioExecution> {
const { faultHits, result } = await runMatrixQaFaultedE2eeBootstrap(context);
const bootstrapError = assertMatrixQaExpectedBootstrapFailure({ faultHits, result });
return {
artifacts: {
bootstrapActor: "driver",
bootstrapErrorPreview: truncateUtf16Safe(bootstrapError, 240),
bootstrapSuccess: result.success,
faultedEndpoint: MATRIX_QA_ROOM_KEY_BACKUP_VERSION_ENDPOINT,
faultHitCount: faultHits.length,
...(faultHits[0]?.ruleId ? { faultRuleId: faultHits[0].ruleId } : {}),
},
details: [
"Matrix E2EE bootstrap failure surfaced through real SDK bootstrap.",
`faulted endpoint: GET ${MATRIX_QA_ROOM_KEY_BACKUP_VERSION_ENDPOINT}`,
`fault hits: ${faultHits.length}`,
`bootstrap success: ${result.success ? "yes" : "no"}`,
`bootstrap error: ${bootstrapError || "<none>"}`,
].join("\n"),
};
}

View File

@@ -0,0 +1,488 @@
// Qa Matrix plugin module implements room and fault scenario runtime E2EE behavior.
import { randomUUID } from "node:crypto";
import { createMatrixQaClient } from "../../substrate/client.js";
import {
createMatrixQaE2eeScenarioClient,
runMatrixQaE2eeBootstrap,
} from "../../substrate/e2ee-client.js";
import type { MatrixQaE2eeScenarioClient } from "../../substrate/e2ee-client.js";
import type { MatrixQaObservedEvent } from "../../substrate/events.js";
import {
startMatrixQaFaultProxy,
type MatrixQaFaultProxyHit,
type MatrixQaFaultProxyRule,
} from "../../substrate/fault-proxy.js";
import {
buildMatrixQaE2eeScenarioRoomKey,
type MatrixQaE2eeScenarioId,
} from "./scenario-catalog.js";
import {
isMatrixQaPlainRecord,
patchMatrixQaGatewayMatrixAccount,
readMatrixQaGatewayMatrixAccount,
} from "./scenario-runtime-config.js";
import {
MATRIX_QA_KEYS_SIGNATURES_UPLOAD_ENDPOINT,
MATRIX_QA_OWNER_SIGNATURE_UPLOAD_BLOCKED_RULE_ID,
MATRIX_QA_ROOM_KEY_BACKUP_FAULT_RULE_ID,
MATRIX_QA_ROOM_KEY_BACKUP_VERSION_ENDPOINT,
MATRIX_QA_SYNC_ENDPOINT,
MATRIX_QA_SYNC_STATE_AFTER_FAULT_RULE_ID,
MATRIX_QA_SYNC_STATE_AFTER_KEY,
MATRIX_QA_SYNC_STATE_AFTER_PARAM,
createMatrixQaE2eeDriverClient,
requireMatrixQaE2eeOutputDir,
requireMatrixQaGatewayConfigPath,
registerMatrixQaE2eeScenarioAccount,
resolveMatrixQaE2eeScenarioGroupRoom,
waitForMatrixQaNonEmptyRoomKeyRestore,
} from "./scenario-runtime-e2ee-shared.js";
import {
assertTopLevelReplyArtifact,
buildMatrixQaToken,
buildMentionPrompt,
doesMatrixQaReplyBodyMatchToken,
isMatrixQaExactMarkerReply,
type MatrixQaScenarioContext,
} from "./scenario-runtime-shared.js";
import type { MatrixQaReplyArtifact } from "./scenario-types.js";
type MatrixQaE2eeBootstrapResult = Awaited<ReturnType<typeof runMatrixQaE2eeBootstrap>>;
export function buildMatrixE2eeReplyArtifact(
event: MatrixQaObservedEvent,
token: string,
): MatrixQaReplyArtifact {
return {
eventId: event.eventId,
mentions: event.mentions,
relatesTo: event.relatesTo,
sender: event.sender,
tokenMatched: doesMatrixQaReplyBodyMatchToken(event, token),
};
}
export function buildRoomKeyBackupUnavailableFaultRule(
accessToken: string,
): MatrixQaFaultProxyRule {
return {
id: MATRIX_QA_ROOM_KEY_BACKUP_FAULT_RULE_ID,
match: (request) =>
request.method === "GET" &&
request.path === MATRIX_QA_ROOM_KEY_BACKUP_VERSION_ENDPOINT &&
request.bearerToken === accessToken,
response: () => ({
body: {
errcode: "M_NOT_FOUND",
error: "No current key backup",
},
status: 404,
}),
};
}
function buildOwnerSignatureUploadBlockedFaultRule(accessToken: string): MatrixQaFaultProxyRule {
return {
id: MATRIX_QA_OWNER_SIGNATURE_UPLOAD_BLOCKED_RULE_ID,
match: (request) =>
request.method === "POST" &&
request.path === MATRIX_QA_KEYS_SIGNATURES_UPLOAD_ENDPOINT &&
request.bearerToken === accessToken,
response: () => ({
body: {},
status: 200,
}),
};
}
function removeMatrixQaSyncStateAfterEncryptionEvents(payload: unknown) {
if (!isMatrixQaPlainRecord(payload)) {
return 0;
}
const rooms = isMatrixQaPlainRecord(payload.rooms) ? payload.rooms : {};
const join = isMatrixQaPlainRecord(rooms.join) ? rooms.join : {};
let removed = 0;
for (const room of Object.values(join)) {
if (!isMatrixQaPlainRecord(room)) {
continue;
}
const stateAfter = room[MATRIX_QA_SYNC_STATE_AFTER_KEY];
if (!isMatrixQaPlainRecord(stateAfter) || !Array.isArray(stateAfter.events)) {
continue;
}
const filtered = stateAfter.events.filter((event) => {
if (isMatrixQaPlainRecord(event) && event.type === "m.room.encryption") {
removed += 1;
return false;
}
return true;
});
stateAfter.events = filtered;
}
return removed;
}
export function buildSyncStateAfterMissingEncryptionFaultRule(
accessToken: string,
): MatrixQaFaultProxyRule {
return {
id: MATRIX_QA_SYNC_STATE_AFTER_FAULT_RULE_ID,
match: (request) =>
request.method === "GET" &&
request.path === MATRIX_QA_SYNC_ENDPOINT &&
request.bearerToken === accessToken &&
new URLSearchParams(request.search).get(MATRIX_QA_SYNC_STATE_AFTER_PARAM) === "true",
mutateResponse: ({ response }) => {
const contentType = response.headers.get("content-type") ?? "";
if (!contentType.includes("json")) {
return response;
}
const payload = JSON.parse(response.body.toString("utf8")) as unknown;
removeMatrixQaSyncStateAfterEncryptionEvents(payload);
return {
...response,
body: Buffer.from(JSON.stringify(payload)),
};
},
};
}
export async function runMatrixQaFaultedE2eeBootstrap(context: MatrixQaScenarioContext): Promise<{
faultHits: MatrixQaFaultProxyHit[];
result: MatrixQaE2eeBootstrapResult;
}> {
const proxy = await startMatrixQaFaultProxy({
targetBaseUrl: context.faultProxyTargetBaseUrl ?? context.baseUrl,
...context.faultProxyObserver,
rules: [buildRoomKeyBackupUnavailableFaultRule(context.driverAccessToken)],
});
try {
const result = await runMatrixQaE2eeBootstrap({
accessToken: context.driverAccessToken,
actorId: "driver",
baseUrl: proxy.baseUrl,
deviceId: context.driverDeviceId,
outputDir: requireMatrixQaE2eeOutputDir(context),
...(context.driverPassword ? { password: context.driverPassword } : {}),
scenarioId: "matrix-e2ee-key-bootstrap-failure",
timeoutMs: context.timeoutMs,
userId: context.driverUserId,
});
return {
faultHits: proxy.hits(),
result,
};
} finally {
await proxy.stop();
}
}
export async function runMatrixQaFaultedRecoveryOwnerVerification(params: {
accessToken: string;
context: MatrixQaScenarioContext;
deviceId: string;
encodedRecoveryKey: string;
userId: string;
}): Promise<{
faultHits: MatrixQaFaultProxyHit[];
restore: Awaited<ReturnType<MatrixQaE2eeScenarioClient["restoreRoomKeyBackup"]>>;
verification: Awaited<ReturnType<MatrixQaE2eeScenarioClient["verifyWithRecoveryKey"]>>;
}> {
const proxy = await startMatrixQaFaultProxy({
targetBaseUrl: params.context.faultProxyTargetBaseUrl ?? params.context.baseUrl,
...params.context.faultProxyObserver,
rules: [buildOwnerSignatureUploadBlockedFaultRule(params.accessToken)],
});
const recoveryClient = await createMatrixQaE2eeScenarioClient({
accessToken: params.accessToken,
actorId: `driver-recovery-${randomUUID().slice(0, 8)}`,
baseUrl: proxy.baseUrl,
deviceId: params.deviceId,
observedEvents: params.context.observedEvents,
outputDir: requireMatrixQaE2eeOutputDir(params.context),
scenarioId: "matrix-e2ee-recovery-owner-verification-required",
timeoutMs: params.context.timeoutMs,
userId: params.userId,
});
try {
const verification = await recoveryClient.verifyWithRecoveryKey(params.encodedRecoveryKey);
const restore = await waitForMatrixQaNonEmptyRoomKeyRestore({
client: recoveryClient,
recoveryKey: params.encodedRecoveryKey,
timeoutMs: params.context.timeoutMs,
});
return {
faultHits: proxy.hits(),
restore,
verification,
};
} finally {
await recoveryClient.stop().catch(() => undefined);
await proxy.stop();
}
}
export function assertMatrixQaFaultedRecoveryOwnerVerificationRequired(
faulted: Awaited<ReturnType<typeof runMatrixQaFaultedRecoveryOwnerVerification>>,
) {
if (faulted.faultHits.length === 0) {
throw new Error("Matrix E2EE owner signature fault proxy was not exercised");
}
if (faulted.verification.success) {
throw new Error(
"Matrix E2EE recovery verification unexpectedly succeeded while owner signature upload was blocked",
);
}
if (!faulted.verification.recoveryKeyAccepted) {
throw new Error("Matrix E2EE recovery key was not accepted");
}
if (!faulted.verification.backupUsable) {
throw new Error("Matrix E2EE recovery key did not leave room-key backup usable");
}
if (faulted.verification.deviceOwnerVerified) {
throw new Error("Matrix E2EE recovery device should still require Matrix identity trust");
}
if (!faulted.restore.success) {
throw new Error(
`Matrix E2EE room-key backup restore failed after owner-verification fault: ${faulted.restore.error ?? "unknown error"}`,
);
}
}
export function assertMatrixQaExpectedBootstrapFailure(params: {
faultHits: MatrixQaFaultProxyHit[];
result: MatrixQaE2eeBootstrapResult;
}) {
if (params.faultHits.length === 0) {
throw new Error("Matrix E2EE bootstrap fault proxy was not exercised");
}
if (params.result.success) {
throw new Error(
"Matrix E2EE bootstrap unexpectedly succeeded while room-key backup was faulted",
);
}
const bootstrapError = params.result.error ?? "";
if (!bootstrapError.toLowerCase().includes("room key backup")) {
throw new Error(`Matrix E2EE bootstrap failed for an unexpected reason: ${bootstrapError}`);
}
return bootstrapError;
}
export async function withMatrixQaE2eeDriver<T>(
context: MatrixQaScenarioContext,
scenarioId: MatrixQaE2eeScenarioId,
run: (client: MatrixQaE2eeScenarioClient) => Promise<T>,
opts: { actorId?: "driver" | `driver-${string}` } = {},
) {
const client = await createMatrixQaE2eeDriverClient(context, scenarioId, opts);
try {
return await run(client);
} finally {
await client.stop();
}
}
async function createMatrixQaE2eeRegisteredScenarioClient(params: {
account: Awaited<ReturnType<typeof registerMatrixQaE2eeScenarioAccount>>;
actorId: `driver-${string}`;
context: MatrixQaScenarioContext;
scenarioId: MatrixQaE2eeScenarioId;
}) {
return await createMatrixQaE2eeScenarioClient({
accessToken: params.account.accessToken,
actorId: params.actorId,
baseUrl: params.context.baseUrl,
deviceId: params.account.deviceId,
observedEvents: params.context.observedEvents,
outputDir: requireMatrixQaE2eeOutputDir(params.context),
password: params.account.password,
scenarioId: params.scenarioId,
timeoutMs: params.context.timeoutMs,
userId: params.account.userId,
});
}
export async function withMatrixQaIsolatedE2eeDriverRoom<T>(
context: MatrixQaScenarioContext,
scenarioId: MatrixQaE2eeScenarioId,
run: (params: {
client: MatrixQaE2eeScenarioClient;
driverUserId: string;
roomId: string;
roomKey: string;
}) => Promise<T>,
) {
if (!context.restartGatewayAfterStateMutation) {
throw new Error(
"Matrix E2EE isolated driver room scenario requires hard gateway restart support",
);
}
const accountId = context.sutAccountId ?? "sut";
const configPath = requireMatrixQaGatewayConfigPath(context);
const accountConfig = await readMatrixQaGatewayMatrixAccount({
accountId,
configPath,
});
const originalGroups = isMatrixQaPlainRecord(accountConfig.groups) ? accountConfig.groups : {};
const originalGroupAllowFrom = Array.isArray(accountConfig.groupAllowFrom)
? accountConfig.groupAllowFrom
: undefined;
const originalGroupPolicy = accountConfig.groupPolicy;
const driverAccount = await registerMatrixQaE2eeScenarioAccount({
context,
deviceName: "OpenClaw Matrix QA Isolated E2EE Driver",
localpartPrefix: "qa-e2ee-driver",
scenarioId,
});
const driverApi = createMatrixQaClient({
accessToken: driverAccount.accessToken,
baseUrl: context.baseUrl,
});
const roomKey = buildMatrixQaE2eeScenarioRoomKey(scenarioId);
const roomId = await driverApi.createPrivateRoom({
encrypted: true,
inviteUserIds: [context.observerUserId, context.sutUserId],
name: `Matrix QA ${scenarioId} Isolated E2EE Room`,
});
await Promise.all([
createMatrixQaClient({
accessToken: context.observerAccessToken,
baseUrl: context.baseUrl,
}).joinRoom(roomId),
createMatrixQaClient({
accessToken: context.sutAccessToken,
baseUrl: context.baseUrl,
}).joinRoom(roomId),
]);
const isolatedGroups = {
[roomId]: {
enabled: true,
requireMention: true,
},
};
const applyPatch = async (accountPatch: Record<string, unknown>) => {
await context.restartGatewayAfterStateMutation?.(
async () => {
await patchMatrixQaGatewayMatrixAccount({
accountId,
accountPatch,
configPath,
});
},
{
timeoutMs: context.timeoutMs,
waitAccountId: accountId,
},
);
};
let patchedGateway;
let client: MatrixQaE2eeScenarioClient | undefined;
try {
await applyPatch({
groupAllowFrom: [driverAccount.userId],
groupPolicy: "allowlist",
groups: isolatedGroups,
});
patchedGateway = true;
const actorId: `driver-${string}` = `driver-${scenarioId
.replace(/^matrix-e2ee-/, "")
.replace(/[^A-Za-z0-9_-]/g, "-")
.slice(0, 28)}`;
client = await createMatrixQaE2eeRegisteredScenarioClient({
account: driverAccount,
actorId,
context,
scenarioId,
});
await Promise.all([
client.waitForJoinedMember({
roomId,
timeoutMs: context.timeoutMs,
userId: context.sutUserId,
}),
client.waitForJoinedMember({
roomId,
timeoutMs: context.timeoutMs,
userId: context.observerUserId,
}),
]);
return await run({
client,
driverUserId: driverAccount.userId,
roomId,
roomKey,
});
} finally {
await client?.stop().catch(() => undefined);
if (patchedGateway) {
const restorePatch: Record<string, unknown> = {
groupAllowFrom: originalGroupAllowFrom,
groupPolicy: originalGroupPolicy,
groups: originalGroups,
};
await applyPatch(restorePatch).catch(() => undefined);
}
}
}
export async function runMatrixQaE2eeTopLevelWithClient(
context: MatrixQaScenarioContext,
params: {
client: MatrixQaE2eeScenarioClient;
driverUserId: string;
roomId: string;
roomKey: string;
tokenPrefix: string;
},
) {
const startSince = await params.client.prime();
const token = buildMatrixQaToken(params.tokenPrefix);
const body = buildMentionPrompt(context.sutUserId, token);
const driverEventId = await params.client.sendTextMessage({
body,
mentionUserIds: [context.sutUserId],
roomId: params.roomId,
});
const matched = await params.client.waitForRoomEvent({
predicate: (event) =>
isMatrixQaExactMarkerReply(event, {
roomId: params.roomId,
sutUserId: context.sutUserId,
token,
}) && event.relatesTo === undefined,
roomId: params.roomId,
timeoutMs: context.timeoutMs,
});
const reply = buildMatrixE2eeReplyArtifact(matched.event, token);
assertTopLevelReplyArtifact("E2EE reply", reply);
return {
driverEventId,
driverUserId: params.driverUserId,
reply,
roomId: params.roomId,
roomKey: params.roomKey,
since: matched.since ?? startSince,
token,
};
}
export async function runMatrixQaE2eeTopLevelScenario(
context: MatrixQaScenarioContext,
params: {
scenarioId: MatrixQaE2eeScenarioId;
tokenPrefix: string;
},
) {
const { roomId, roomKey } = resolveMatrixQaE2eeScenarioGroupRoom(context, params.scenarioId);
return await withMatrixQaE2eeDriver(context, params.scenarioId, async (client) => {
return await runMatrixQaE2eeTopLevelWithClient(context, {
client,
driverUserId: context.driverUserId,
roomId,
roomKey,
tokenPrefix: params.tokenPrefix,
});
});
}

View File

@@ -0,0 +1,422 @@
// Qa Matrix plugin module implements shared scenario runtime E2EE behavior.
import { randomUUID } from "node:crypto";
import { setTimeout as sleep } from "node:timers/promises";
import type { MatrixVerificationSummary } from "@openclaw/matrix/test-api.js";
import { createMatrixQaClient } from "../../substrate/client.js";
import {
createMatrixQaE2eeScenarioClient,
runMatrixQaE2eeBootstrap,
} from "../../substrate/e2ee-client.js";
import type { MatrixQaE2eeScenarioClient } from "../../substrate/e2ee-client.js";
import type { MatrixQaObservedEvent } from "../../substrate/events.js";
import {
buildMatrixQaE2eeScenarioRoomKey,
type MatrixQaE2eeScenarioId,
resolveMatrixQaScenarioRoomId,
} from "./scenario-catalog.js";
import type { MatrixQaScenarioContext } from "./scenario-runtime-shared.js";
export const MATRIX_QA_ROOM_KEY_BACKUP_VERSION_ENDPOINT = "/_matrix/client/v3/room_keys/version";
export const MATRIX_QA_ROOM_KEY_BACKUP_FAULT_RULE_ID = "room-key-backup-version-unavailable";
export const MATRIX_QA_OWNER_SIGNATURE_UPLOAD_BLOCKED_RULE_ID = "owner-signature-upload-blocked";
export const MATRIX_QA_KEYS_SIGNATURES_UPLOAD_ENDPOINT =
"/_matrix/client/v3/keys/signatures/upload";
export const MATRIX_QA_SYNC_ENDPOINT = "/_matrix/client/v3/sync";
export const MATRIX_QA_SYNC_STATE_AFTER_FAULT_RULE_ID = "sync-state-after-missing-encryption";
export const MATRIX_QA_SYNC_STATE_AFTER_KEY = "org.matrix.msc4222.state_after";
export const MATRIX_QA_SYNC_STATE_AFTER_PARAM = "org.matrix.msc4222.use_state_after";
type MatrixQaE2eeBootstrapResult = Awaited<ReturnType<typeof runMatrixQaE2eeBootstrap>>;
export function requireMatrixQaE2eeOutputDir(context: MatrixQaScenarioContext) {
if (!context.outputDir) {
throw new Error("Matrix E2EE QA scenarios require an output directory");
}
return context.outputDir;
}
export function requireMatrixQaCliRuntimeEnv(context: MatrixQaScenarioContext) {
if (!context.gatewayRuntimeEnv) {
throw new Error("Matrix CLI QA scenarios require the gateway runtime environment");
}
return context.gatewayRuntimeEnv;
}
export function requireMatrixQaGatewayConfigPath(context: MatrixQaScenarioContext) {
const configPath = requireMatrixQaCliRuntimeEnv(context).OPENCLAW_CONFIG_PATH?.trim();
if (!configPath) {
throw new Error("Matrix CLI QA scenarios require the gateway config path");
}
return configPath;
}
export function requireMatrixQaRegistrationToken(context: MatrixQaScenarioContext) {
const token = context.registrationToken?.trim();
if (!token) {
throw new Error("Matrix CLI QA scenarios require the homeserver registration token");
}
return token;
}
export function requireMatrixQaPassword(
context: MatrixQaScenarioContext,
actor: "driver" | "observer" | "sut",
) {
const password =
actor === "driver"
? context.driverPassword
: actor === "observer"
? context.observerPassword
: context.sutPassword;
if (!password) {
throw new Error(`Matrix E2EE ${actor} password is required for this scenario`);
}
return password;
}
export function resolveMatrixQaE2eeScenarioGroupRoom(
context: MatrixQaScenarioContext,
scenarioId: MatrixQaE2eeScenarioId,
) {
const roomKey = buildMatrixQaE2eeScenarioRoomKey(scenarioId);
return {
roomKey,
roomId: resolveMatrixQaScenarioRoomId(context, roomKey),
};
}
export async function registerMatrixQaE2eeScenarioAccount(params: {
context: MatrixQaScenarioContext;
deviceName: string;
localpartPrefix: string;
scenarioId: MatrixQaE2eeScenarioId;
}) {
const localpartSuffix = params.scenarioId
.replace(/^matrix-e2ee-/, "")
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "")
.slice(0, 24);
const account = await createMatrixQaClient({
baseUrl: params.context.baseUrl,
}).registerWithToken({
deviceName: params.deviceName,
localpart: `${params.localpartPrefix}-${localpartSuffix}-${randomUUID().replaceAll("-", "").slice(0, 8)}`,
password: `matrix-qa-${randomUUID()}`,
registrationToken: requireMatrixQaRegistrationToken(params.context),
});
if (!account.deviceId) {
throw new Error(
`Matrix E2EE QA registration for ${params.scenarioId} did not return a device id`,
);
}
return account;
}
export function assertMatrixQaBootstrapSucceeded(
label: string,
result: MatrixQaE2eeBootstrapResult,
) {
if (!result.success) {
throw new Error(`${label} bootstrap failed: ${result.error ?? "unknown error"}`);
}
if (!result.verification.verified || !result.verification.signedByOwner) {
throw new Error(`${label} bootstrap did not leave the device verified by its owner`);
}
if (!result.verification.crossSigningVerified) {
throw new Error(`${label} bootstrap did not establish full Matrix identity trust`);
}
if (!result.crossSigning.published) {
throw new Error(`${label} bootstrap did not publish cross-signing keys`);
}
if (!result.verification.recoveryKeyStored) {
throw new Error(`${label} bootstrap did not store a recovery key`);
}
if (!result.verification.backupVersion) {
throw new Error(`${label} bootstrap did not create a room-key backup`);
}
}
function isMatrixQaRepairableBackupBootstrapError(error: string | undefined) {
const normalized = error?.toLowerCase() ?? "";
return (
normalized.includes("room key backup is not usable") ||
normalized.includes("m.megolm_backup.v1") ||
normalized.includes("backup decryption key could not be loaded")
);
}
const MATRIX_QA_PRESERVE_IDENTITY_BOOTSTRAP_OPTIONS = {
allowAutomaticCrossSigningReset: false,
} as const;
export async function assertMatrixQaPeerDeviceTrusted(params: {
client: MatrixQaE2eeScenarioClient;
deviceId: string;
label: string;
timeoutMs: number;
userId: string;
}) {
const startedAt = Date.now();
let status = await params.client.getDeviceVerificationStatus(params.userId, params.deviceId);
while (!status.verified && Date.now() - startedAt < params.timeoutMs) {
await sleep(Math.min(250, Math.max(25, params.timeoutMs - (Date.now() - startedAt))));
status = await params.client.getDeviceVerificationStatus(params.userId, params.deviceId);
}
if (!status.verified) {
throw new Error(
`${params.label} did not trust ${params.userId}/${params.deviceId} after verification`,
);
}
return status;
}
export async function ensureMatrixQaE2eeOwnDeviceVerified(params: {
client: MatrixQaE2eeScenarioClient;
label: string;
}) {
let bootstrap = await params.client.bootstrapOwnDeviceVerification(
MATRIX_QA_PRESERVE_IDENTITY_BOOTSTRAP_OPTIONS,
);
if (!bootstrap.success && isMatrixQaRepairableBackupBootstrapError(bootstrap.error)) {
const reset = await params.client.resetRoomKeyBackup();
if (reset.success) {
bootstrap = await params.client.bootstrapOwnDeviceVerification(
MATRIX_QA_PRESERVE_IDENTITY_BOOTSTRAP_OPTIONS,
);
}
}
assertMatrixQaBootstrapSucceeded(params.label, bootstrap);
return {
bootstrap,
recoveryKey: await params.client.getRecoveryKey(),
verification: bootstrap.verification,
};
}
export async function waitForMatrixQaNonEmptyRoomKeyRestore(params: {
client: MatrixQaE2eeScenarioClient;
recoveryKey: string;
timeoutMs: number;
}) {
const startedAt = Date.now();
let last: Awaited<ReturnType<MatrixQaE2eeScenarioClient["restoreRoomKeyBackup"]>> | null = null;
while (Date.now() - startedAt < params.timeoutMs) {
const restored = await params.client.restoreRoomKeyBackup({
recoveryKey: params.recoveryKey,
});
last = restored;
if (!restored.success) {
throw new Error(
`Matrix E2EE room-key backup restore failed: ${restored.error ?? "unknown error"}`,
);
}
if (restored.total > 0 && restored.imported > 0) {
return restored;
}
await sleep(500);
}
throw new Error(
`Matrix E2EE room-key backup restore did not import any keys before timeout (last imported/total: ${last?.imported ?? 0}/${last?.total ?? 0})`,
);
}
export async function waitForMatrixQaVerificationSummary(params: {
client: MatrixQaE2eeScenarioClient;
label: string;
predicate: (summary: MatrixVerificationSummary) => boolean;
timeoutMs: number;
}) {
const startedAt = Date.now();
while (Date.now() - startedAt < params.timeoutMs) {
const summaries = await params.client.listVerifications();
const found = summaries.find(params.predicate);
if (found) {
return found;
}
await sleep(Math.min(250, Math.max(25, params.timeoutMs - (Date.now() - startedAt))));
}
throw new Error(`timed out waiting for Matrix verification summary: ${params.label}`);
}
export function sameMatrixQaVerificationTransaction(
left: MatrixVerificationSummary,
right: MatrixVerificationSummary,
) {
return Boolean(left.transactionId && left.transactionId === right.transactionId);
}
export function formatMatrixQaSasEmoji(summary: MatrixVerificationSummary) {
return summary.sas?.emoji?.map(([emoji, label]) => `${emoji} ${label}`) ?? [];
}
function assertMatrixQaSasEmojiMatches(params: {
initiator: MatrixVerificationSummary;
recipient: MatrixVerificationSummary;
}) {
const initiatorEmoji = formatMatrixQaSasEmoji(params.initiator);
const recipientEmoji = formatMatrixQaSasEmoji(params.recipient);
if (initiatorEmoji.length === 0 || recipientEmoji.length === 0) {
throw new Error("Matrix SAS verification did not expose emoji data on both devices");
}
if (JSON.stringify(initiatorEmoji) !== JSON.stringify(recipientEmoji)) {
throw new Error("Matrix SAS emoji did not match between verification devices");
}
return initiatorEmoji;
}
export function isMatrixQaE2eeNoticeTriggeredSutReply(params: {
event: MatrixQaObservedEvent;
noticeEventId: string;
noticeSentAt: number;
roomId: string;
sutUserId: string;
token: string;
}) {
if (
params.event.roomId !== params.roomId ||
params.event.sender !== params.sutUserId ||
params.event.type !== "m.room.message"
) {
return false;
}
if (params.event.body?.includes(params.token)) {
return true;
}
if (
params.event.relatesTo?.eventId === params.noticeEventId ||
params.event.relatesTo?.inReplyToId === params.noticeEventId
) {
return true;
}
return (
typeof params.event.originServerTs === "number" &&
params.event.originServerTs >= params.noticeSentAt
);
}
export async function createMatrixQaE2eeDriverClient(
context: MatrixQaScenarioContext,
scenarioId: MatrixQaE2eeScenarioId,
opts: { actorId?: "driver" | `driver-${string}` } = {},
) {
return await createMatrixQaE2eeScenarioClient({
accessToken: context.driverAccessToken,
actorId: opts.actorId ?? "driver",
baseUrl: context.baseUrl,
deviceId: context.driverDeviceId,
observedEvents: context.observedEvents,
outputDir: requireMatrixQaE2eeOutputDir(context),
password: context.driverPassword,
scenarioId,
timeoutMs: context.timeoutMs,
userId: context.driverUserId,
});
}
async function createMatrixQaE2eeObserverClient(
context: MatrixQaScenarioContext,
scenarioId: MatrixQaE2eeScenarioId,
) {
return await createMatrixQaE2eeScenarioClient({
accessToken: context.observerAccessToken,
actorId: "observer",
baseUrl: context.baseUrl,
deviceId: context.observerDeviceId,
observedEvents: context.observedEvents,
outputDir: requireMatrixQaE2eeOutputDir(context),
password: context.observerPassword,
scenarioId,
timeoutMs: context.timeoutMs,
userId: context.observerUserId,
});
}
export async function withMatrixQaE2eeDriverAndObserver<T>(
context: MatrixQaScenarioContext,
scenarioId: MatrixQaE2eeScenarioId,
run: (clients: {
driver: MatrixQaE2eeScenarioClient;
observer: MatrixQaE2eeScenarioClient;
}) => Promise<T>,
) {
const driver = await createMatrixQaE2eeDriverClient(context, scenarioId);
const observer = await createMatrixQaE2eeObserverClient(context, scenarioId);
try {
return await run({ driver, observer });
} finally {
await Promise.all([driver.stop(), observer.stop()]);
}
}
export async function completeMatrixQaSasVerification(params: {
initiator: MatrixQaE2eeScenarioClient;
recipient: MatrixQaE2eeScenarioClient;
recipientUserId: string;
request: {
deviceId?: string;
roomId?: string;
userId: string;
};
timeoutMs: number;
}) {
const initiated = await params.initiator.requestVerification(params.request);
const recipientRequested = await waitForMatrixQaVerificationSummary({
client: params.recipient,
label: "recipient request",
predicate: (summary) =>
!summary.initiatedByMe &&
(sameMatrixQaVerificationTransaction(summary, initiated) ||
(summary.otherUserId !== params.recipientUserId && summary.pending)),
timeoutMs: params.timeoutMs,
});
if (recipientRequested.canAccept) {
await params.recipient.acceptVerification(recipientRequested.id);
}
await waitForMatrixQaVerificationSummary({
client: params.initiator,
label: "initiator ready",
predicate: (summary) =>
sameMatrixQaVerificationTransaction(summary, initiated) && summary.phaseName === "ready",
timeoutMs: params.timeoutMs,
});
await params.initiator.startVerification(initiated.id, "sas");
const initiatorSas = await waitForMatrixQaVerificationSummary({
client: params.initiator,
label: "initiator SAS",
predicate: (summary) =>
sameMatrixQaVerificationTransaction(summary, initiated) && summary.hasSas,
timeoutMs: params.timeoutMs,
});
const recipientSas = await waitForMatrixQaVerificationSummary({
client: params.recipient,
label: "recipient SAS",
predicate: (summary) =>
sameMatrixQaVerificationTransaction(summary, initiatorSas) && summary.hasSas,
timeoutMs: params.timeoutMs,
});
const sasEmoji = assertMatrixQaSasEmojiMatches({
initiator: initiatorSas,
recipient: recipientSas,
});
await params.initiator.confirmVerificationSas(initiatorSas.id);
await params.recipient.confirmVerificationSas(recipientSas.id);
const completedInitiator = await waitForMatrixQaVerificationSummary({
client: params.initiator,
label: "initiator complete",
predicate: (summary) =>
sameMatrixQaVerificationTransaction(summary, initiated) && summary.completed,
timeoutMs: params.timeoutMs,
});
const completedRecipient = await waitForMatrixQaVerificationSummary({
client: params.recipient,
label: "recipient complete",
predicate: (summary) =>
sameMatrixQaVerificationTransaction(summary, completedInitiator) && summary.completed,
timeoutMs: params.timeoutMs,
});
return {
completedInitiator,
completedRecipient,
sasEmoji,
};
}

View File

@@ -0,0 +1,312 @@
// Qa Matrix plugin module implements verification scenario runtime E2EE behavior.
import { createMatrixQaClient } from "../../substrate/client.js";
import {
MATRIX_QA_E2EE_VERIFICATION_DM_ROOM_KEY,
resolveMatrixQaScenarioRoomId,
} from "./scenario-catalog.js";
import { withMatrixQaE2eeDriver } from "./scenario-runtime-e2ee-room.js";
import {
assertMatrixQaPeerDeviceTrusted,
completeMatrixQaSasVerification,
ensureMatrixQaE2eeOwnDeviceVerified,
requireMatrixQaPassword,
sameMatrixQaVerificationTransaction,
waitForMatrixQaVerificationSummary,
withMatrixQaE2eeDriverAndObserver,
} from "./scenario-runtime-e2ee-shared.js";
import type { MatrixQaScenarioContext } from "./scenario-runtime-shared.js";
import type { MatrixQaScenarioExecution } from "./scenario-types.js";
export async function runMatrixQaE2eeDeviceSasVerificationScenario(
context: MatrixQaScenarioContext,
): Promise<MatrixQaScenarioExecution> {
requireMatrixQaPassword(context, "driver");
requireMatrixQaPassword(context, "observer");
if (!context.observerDeviceId) {
throw new Error("Matrix E2EE observer device id is required for device SAS verification");
}
if (!context.driverDeviceId) {
throw new Error("Matrix E2EE driver device id is required for device SAS verification");
}
const observerDeviceId = context.observerDeviceId;
const driverDeviceId = context.driverDeviceId;
return await withMatrixQaE2eeDriverAndObserver(
context,
"matrix-e2ee-device-sas-verification",
async ({ driver, observer }) => {
await Promise.all([
ensureMatrixQaE2eeOwnDeviceVerified({
client: driver,
label: "driver",
}),
ensureMatrixQaE2eeOwnDeviceVerified({
client: observer,
label: "observer",
}),
]);
const result = await completeMatrixQaSasVerification({
initiator: driver,
recipient: observer,
recipientUserId: context.observerUserId,
request: {
deviceId: observerDeviceId,
userId: context.observerUserId,
},
timeoutMs: context.timeoutMs,
});
const driverTrust = await assertMatrixQaPeerDeviceTrusted({
client: driver,
deviceId: observerDeviceId,
label: "driver",
timeoutMs: context.timeoutMs,
userId: context.observerUserId,
});
const observerTrust = await assertMatrixQaPeerDeviceTrusted({
client: observer,
deviceId: driverDeviceId,
label: "observer",
timeoutMs: context.timeoutMs,
userId: context.driverUserId,
});
return {
artifacts: {
completedVerificationIds: [result.completedInitiator.id, result.completedRecipient.id],
currentDeviceId: driverDeviceId,
driverTrustsObserverDevice: driverTrust.verified,
observerTrustsDriverDevice: observerTrust.verified,
sasEmoji: result.sasEmoji,
secondaryDeviceId: observerDeviceId,
},
details: [
"driver-to-observer device verification completed with real SAS",
`initiator transaction: ${result.completedInitiator.transactionId ?? "<none>"}`,
`recipient transaction: ${result.completedRecipient.transactionId ?? "<none>"}`,
`driver trusts observer device: ${driverTrust.verified ? "yes" : "no"}`,
`observer trusts driver device: ${observerTrust.verified ? "yes" : "no"}`,
`emoji: ${result.sasEmoji.join(", ")}`,
].join("\n"),
};
},
);
}
export async function runMatrixQaE2eeQrVerificationScenario(
context: MatrixQaScenarioContext,
): Promise<MatrixQaScenarioExecution> {
requireMatrixQaPassword(context, "driver");
requireMatrixQaPassword(context, "observer");
if (!context.observerDeviceId) {
throw new Error("Matrix E2EE observer device id is required for QR verification");
}
if (!context.driverDeviceId) {
throw new Error("Matrix E2EE driver device id is required for QR verification");
}
const observerDeviceId = context.observerDeviceId;
const driverDeviceId = context.driverDeviceId;
return await withMatrixQaE2eeDriverAndObserver(
context,
"matrix-e2ee-qr-verification",
async ({ driver, observer }) => {
await Promise.all([
ensureMatrixQaE2eeOwnDeviceVerified({
client: driver,
label: "driver",
}),
ensureMatrixQaE2eeOwnDeviceVerified({
client: observer,
label: "observer",
}),
]);
const initiated = await driver.requestVerification({
deviceId: observerDeviceId,
userId: context.observerUserId,
});
const incoming = await waitForMatrixQaVerificationSummary({
client: observer,
label: "QR recipient request",
predicate: (summary) =>
!summary.initiatedByMe && sameMatrixQaVerificationTransaction(summary, initiated),
timeoutMs: context.timeoutMs,
});
if (incoming.canAccept) {
await observer.acceptVerification(incoming.id);
}
await waitForMatrixQaVerificationSummary({
client: driver,
label: "QR request ready",
predicate: (summary) =>
sameMatrixQaVerificationTransaction(summary, initiated) && summary.phaseName === "ready",
timeoutMs: context.timeoutMs,
});
const qr = await driver.generateVerificationQr(initiated.id);
await observer.scanVerificationQr(incoming.id, qr.qrDataBase64);
const reciprocate = await waitForMatrixQaVerificationSummary({
client: driver,
label: "QR reciprocate",
predicate: (summary) =>
sameMatrixQaVerificationTransaction(summary, initiated) && summary.hasReciprocateQr,
timeoutMs: context.timeoutMs,
});
await driver.confirmVerificationReciprocateQr(reciprocate.id);
const qrByteCount = Buffer.from(qr.qrDataBase64, "base64").byteLength;
const completedDriver = await waitForMatrixQaVerificationSummary({
client: driver,
label: "QR driver complete",
predicate: (summary) =>
sameMatrixQaVerificationTransaction(summary, initiated) && summary.completed,
timeoutMs: context.timeoutMs,
});
const completedObserver = await waitForMatrixQaVerificationSummary({
client: observer,
label: "QR observer complete",
predicate: (summary) =>
sameMatrixQaVerificationTransaction(summary, completedDriver) && summary.completed,
timeoutMs: context.timeoutMs,
});
const driverTrust = await assertMatrixQaPeerDeviceTrusted({
client: driver,
deviceId: observerDeviceId,
label: "driver",
timeoutMs: context.timeoutMs,
userId: context.observerUserId,
});
const observerTrust = await assertMatrixQaPeerDeviceTrusted({
client: observer,
deviceId: driverDeviceId,
label: "observer",
timeoutMs: context.timeoutMs,
userId: context.driverUserId,
});
return {
artifacts: {
completedVerificationIds: [completedDriver.id, completedObserver.id],
driverTrustsObserverDevice: driverTrust.verified,
identityVerificationCompleted: true,
observerTrustsDriverDevice: observerTrust.verified,
qrBytes: qrByteCount,
secondaryDeviceId: observerDeviceId,
},
details: [
"driver-to-observer QR verification completed through real QR scan",
`transaction: ${completedDriver.transactionId ?? "<none>"}`,
`driver trusts observer device: ${driverTrust.verified ? "yes" : "no"}`,
`observer trusts driver device: ${observerTrust.verified ? "yes" : "no"}`,
`qr bytes: ${qrByteCount}`,
].join("\n"),
};
},
);
}
export async function runMatrixQaE2eeStaleDeviceHygieneScenario(
context: MatrixQaScenarioContext,
): Promise<MatrixQaScenarioExecution> {
const driverPassword = requireMatrixQaPassword(context, "driver");
return await withMatrixQaE2eeDriver(
context,
"matrix-e2ee-stale-device-hygiene",
async (client) => {
await ensureMatrixQaE2eeOwnDeviceVerified({
client,
label: "driver",
});
const loginClient = createMatrixQaClient({
baseUrl: context.baseUrl,
});
const secondary = await loginClient.loginWithPassword({
deviceName: "OpenClaw Matrix QA Stale Device",
password: driverPassword,
userId: context.driverUserId,
});
if (!secondary.deviceId) {
throw new Error("Matrix stale-device login did not return a secondary device id");
}
const before = await client.listOwnDevices();
if (!before.some((device) => device.deviceId === secondary.deviceId)) {
throw new Error("Matrix stale-device list did not include the secondary login");
}
await client.stop().catch(() => undefined);
const deleted = await client.deleteOwnDevices([secondary.deviceId]);
const remainingDeviceIds = deleted.remainingDevices.map((device) => device.deviceId);
if (remainingDeviceIds.includes(secondary.deviceId)) {
throw new Error(
"Matrix stale-device deletion left the secondary device in the device list",
);
}
if (
deleted.currentDeviceId &&
!deleted.remainingDevices.some((device) => device.deviceId === deleted.currentDeviceId)
) {
throw new Error("Matrix stale-device deletion removed the current device");
}
return {
artifacts: {
currentDeviceId: deleted.currentDeviceId,
deletedDeviceIds: deleted.deletedDeviceIds,
remainingDeviceIds,
secondaryDeviceId: secondary.deviceId,
},
details: [
"driver secondary device was created, observed, and removed through real device APIs",
`current device: ${deleted.currentDeviceId ?? "<none>"}`,
`deleted device: ${secondary.deviceId}`,
`remaining devices: ${remainingDeviceIds.join(", ")}`,
].join("\n"),
};
},
);
}
export async function runMatrixQaE2eeDmSasVerificationScenario(
context: MatrixQaScenarioContext,
): Promise<MatrixQaScenarioExecution> {
requireMatrixQaPassword(context, "driver");
requireMatrixQaPassword(context, "observer");
const roomId = resolveMatrixQaScenarioRoomId(context, MATRIX_QA_E2EE_VERIFICATION_DM_ROOM_KEY);
return await withMatrixQaE2eeDriverAndObserver(
context,
"matrix-e2ee-dm-sas-verification",
async ({ driver, observer }) => {
await Promise.all([
ensureMatrixQaE2eeOwnDeviceVerified({
client: driver,
label: "driver",
}),
ensureMatrixQaE2eeOwnDeviceVerified({
client: observer,
label: "observer",
}),
]);
const result = await completeMatrixQaSasVerification({
initiator: driver,
recipient: observer,
recipientUserId: context.observerUserId,
request: {
roomId,
userId: context.observerUserId,
},
timeoutMs: context.timeoutMs,
});
if (
result.completedInitiator.roomId !== roomId ||
result.completedRecipient.roomId !== roomId
) {
throw new Error("Matrix E2EE DM verification completed outside the expected DM room");
}
return {
artifacts: {
completedVerificationIds: [result.completedInitiator.id, result.completedRecipient.id],
roomKey: MATRIX_QA_E2EE_VERIFICATION_DM_ROOM_KEY,
sasEmoji: result.sasEmoji,
verificationRoomId: roomId,
},
details: [
"driver/observer encrypted DM verification completed with SAS in the expected room",
`verification DM room: ${roomId}`,
`transaction: ${result.completedInitiator.transactionId ?? "<none>"}`,
`emoji: ${result.sasEmoji.join(", ")}`,
].join("\n"),
};
},
);
}

View File

@@ -21,6 +21,21 @@ import {
runApprovalPluginMetadataSingleEventScenario,
runApprovalThreadTargetScenario,
} from "./scenario-runtime-approval.js";
import {
runMatrixQaE2eeCliAccountAddEnableE2eeScenario,
runMatrixQaE2eeCliEncryptionSetupBootstrapFailureScenario,
runMatrixQaE2eeCliEncryptionSetupIdempotentScenario,
runMatrixQaE2eeCliEncryptionSetupScenario,
} from "./scenario-runtime-e2ee-cli-account.js";
import {
runMatrixQaE2eeCliEncryptionSetupMultiAccountScenario,
runMatrixQaE2eeCliSetupThenGatewayReplyScenario,
} from "./scenario-runtime-e2ee-cli-gateway.js";
import {
runMatrixQaE2eeCliRecoveryKeyInvalidScenario,
runMatrixQaE2eeCliRecoveryKeySetupScenario,
} from "./scenario-runtime-e2ee-cli-recovery.js";
import { runMatrixQaE2eeCliSelfVerificationScenario } from "./scenario-runtime-e2ee-cli-verification.js";
import {
runMatrixQaE2eeCorruptCryptoIdbSnapshotScenario,
runMatrixQaE2eeHistoryExistsBackupEmptyScenario,
@@ -38,29 +53,24 @@ import {
import {
runMatrixQaE2eeArtifactRedactionScenario,
runMatrixQaE2eeBasicReplyScenario,
runMatrixQaE2eeBootstrapSuccessScenario,
runMatrixQaE2eeCliAccountAddEnableE2eeScenario,
runMatrixQaE2eeCliEncryptionSetupBootstrapFailureScenario,
runMatrixQaE2eeCliEncryptionSetupIdempotentScenario,
runMatrixQaE2eeCliEncryptionSetupMultiAccountScenario,
runMatrixQaE2eeCliEncryptionSetupScenario,
runMatrixQaE2eeCliRecoveryKeyInvalidScenario,
runMatrixQaE2eeCliRecoveryKeySetupScenario,
runMatrixQaE2eeCliSetupThenGatewayReplyScenario,
runMatrixQaE2eeCliSelfVerificationScenario,
runMatrixQaE2eeDeviceSasVerificationScenario,
runMatrixQaE2eeDmSasVerificationScenario,
runMatrixQaE2eeKeyBootstrapFailureScenario,
runMatrixQaE2eeMediaImageScenario,
runMatrixQaE2eeQrVerificationScenario,
runMatrixQaE2eeRecoveryKeyLifecycleScenario,
runMatrixQaE2eeRecoveryOwnerVerificationRequiredScenario,
runMatrixQaE2eeRestartResumeScenario,
runMatrixQaE2eeStateAfterMissingEncryptionScenario,
runMatrixQaE2eeStaleDeviceHygieneScenario,
runMatrixQaE2eeThreadFollowUpScenario,
runMatrixQaE2eeVerificationNoticeNoTriggerScenario,
} from "./scenario-runtime-e2ee.js";
} from "./scenario-runtime-e2ee-messages.js";
import {
runMatrixQaE2eeBootstrapSuccessScenario,
runMatrixQaE2eeKeyBootstrapFailureScenario,
runMatrixQaE2eeRecoveryKeyLifecycleScenario,
runMatrixQaE2eeRecoveryOwnerVerificationRequiredScenario,
} from "./scenario-runtime-e2ee-recovery.js";
import {
runMatrixQaE2eeDeviceSasVerificationScenario,
runMatrixQaE2eeDmSasVerificationScenario,
runMatrixQaE2eeQrVerificationScenario,
runMatrixQaE2eeStaleDeviceHygieneScenario,
} from "./scenario-runtime-e2ee-verification.js";
import {
runInboundEditIgnoredScenario,
runInboundEditNoDuplicateTriggerScenario,

View File

@@ -5656,7 +5656,7 @@ describe("matrix live qa scenarios", () => {
userId: "@cli-recovery:matrix-qa.test",
});
let initialAccountConfig: Record<string, unknown> | null = null;
runMatrixQaOpenClawCli.mockImplementation(async ({ args, env }) => {
runMatrixQaOpenClawCli.mockImplementation(async ({ args, env, stdin }) => {
if (!initialAccountConfig && env.OPENCLAW_CONFIG_PATH) {
const initialConfig = JSON.parse(
await readFile(String(env.OPENCLAW_CONFIG_PATH), "utf8"),
@@ -5673,8 +5673,9 @@ describe("matrix live qa scenarios", () => {
const joined = args.join(" ");
if (
joined ===
"matrix encryption setup --account cli-recovery-key-setup --recovery-key encoded-recovery-key --json"
"matrix encryption setup --account cli-recovery-key-setup --recovery-key-stdin --json"
) {
expect(stdin).toBe("encoded-recovery-key\n");
return {
args,
exitCode: 0,
@@ -5759,8 +5760,7 @@ describe("matrix live qa scenarios", () => {
"setup",
"--account",
"cli-recovery-key-setup",
"--recovery-key",
"encoded-recovery-key",
"--recovery-key-stdin",
"--json",
],
]);
@@ -5839,6 +5839,8 @@ describe("matrix live qa scenarios", () => {
.fn()
.mockRejectedValue(new Error("openclaw matrix encryption setup exited 1"));
const kill = vi.fn();
const endStdin = vi.fn();
const writeStdin = vi.fn().mockResolvedValue(undefined);
startMatrixQaOpenClawCli.mockReturnValue({
args: [
"matrix",
@@ -5846,15 +5848,15 @@ describe("matrix live qa scenarios", () => {
"setup",
"--account",
"cli-invalid-recovery-key",
"--recovery-key",
"not-a-valid-matrix-recovery-key",
"--recovery-key-stdin",
"--json",
],
endStdin,
kill,
output,
wait,
waitForOutput: vi.fn(),
writeStdin: vi.fn(),
writeStdin,
});
const scenario = requireMatrixQaScenario("matrix-e2ee-cli-recovery-key-invalid");
@@ -5895,10 +5897,11 @@ describe("matrix live qa scenarios", () => {
"setup",
"--account",
"cli-invalid-recovery-key",
"--recovery-key",
"not-a-valid-matrix-recovery-key",
"--recovery-key-stdin",
"--json",
]);
expect(writeStdin).toHaveBeenCalledWith("not-a-valid-matrix-recovery-key\n");
expect(endStdin).toHaveBeenCalledTimes(1);
expect(output).toHaveBeenCalledTimes(1);
expect(wait).toHaveBeenCalledTimes(1);
expect(kill).toHaveBeenCalledTimes(1);