fix: extract matrix password from backtick code span (real conduit format)

The live conduit admin-room reply (observed directly in #admins) is:

  Successfully reset the password for user @x:server: `<password>`

The delimiter is ': ' after the user id, NOT ' to:' — so the ' to: ' marker
added earlier never matched the real format, and extract_new_password returned
None for every reply, timing out auto-recovery for every agent.

conduit always renders the password as a backtick code span, so anchor on
that directly: take the content of the first backtick pair when the message
is a password-reset success. Guard against a code-spanned matrix user id in
an error message (a real password has no whitespace and isn't @localpart:server).
Prose markers stay as a fallback for hypothetical non-code-span builds.

Tests cover the exact live format and the code-spanned-user-id error case.
This commit is contained in:
atlas 2026-06-05 01:49:31 +02:00
commit 2d8a68e4d1

View file

@ -365,6 +365,32 @@ fn extract_new_password(bot_message: &str) -> Option<String> {
// ASCII lowercase: same byte length as the original, so positions from
// `lower.find(marker)` are valid byte indices into `bot_message`.
let lower = bot_message.to_ascii_lowercase();
// Primary strategy: the conduit/tuwunel admin bot always renders the new
// password as a backtick code span. The live reply observed in the admin
// room is:
// "Successfully reset the password for user @x:server: `<password>`"
// (note the delimiter is ": " after the user id, NOT " to:" — the prose
// wording varies between builds, so anchoring on the code span is the
// robust extraction). Take the content of the first backtick pair when the
// message is a password-reset success. Guard against grabbing a code-spanned
// matrix user id ("@x:server") from an error message: a real password has
// no whitespace and isn't a `@localpart:server` id.
if lower.contains("password")
&& let Some(open) = bot_message.find('`')
{
let after = &bot_message[open + 1..];
if let Some(close) = after.find('`') {
let pw = &after[..close];
if !pw.is_empty()
&& !pw.contains(char::is_whitespace)
&& !(pw.starts_with('@') && pw.contains(':'))
{
return Some(pw.to_owned());
}
}
}
for marker in &[
// Explicit "is:" variants (most common in conduwuit / tuwunel):
"new password is: ",
@ -503,6 +529,24 @@ mod extract_new_password_tests {
assert_eq!(extract_new_password(msg).as_deref(), Some("hunter2"));
}
#[test]
fn conduit_live_admin_room_format() {
// The ACTUAL reply observed in the live #admins room — the delimiter
// is ": " after the user id (no " to:"), password in a code span.
let msg = "Successfully reset the password for user @triage:pr1ma.darkest.space: `hVfa6TpvIKnADoEJNWn9saHoI`";
assert_eq!(
extract_new_password(msg).as_deref(),
Some("hVfa6TpvIKnADoEJNWn9saHoI")
);
}
#[test]
fn codespan_userid_in_error_not_mistaken_for_password() {
// An error that code-spans the user id must not yield it as a password.
let msg = "Failed to reset password for `@sock:pr1ma.darkest.space` — user not found";
assert_eq!(extract_new_password(msg), None);
}
#[test]
fn bare_new_password_colon() {
let msg = "New password: P@ssword1";