From defface0a5364189c415a841906d016243dc4b22 Mon Sep 17 00:00:00 2001 From: atlas Date: Thu, 4 Jun 2026 21:27:04 +0200 Subject: [PATCH 1/2] fix: parse tuwunel 'reset password for X to: ' admin reply MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit extract_new_password had markers for 'is:', 'changed to:', 'reset to:', 'set to:' etc. but none match tuwunel's actual reset-password reply: Successfully reset password for @user:server to: Here the verb ('reset') is not adjacent to 'to:', so every marker missed and the function returned None for every polled event. The admin-room poll then ran its full 15s loop without a match and the auto-recovery timed out on every agent — even though the bot replied correctly and the backward poll found the message. This is why matrix password auto-recovery kept looping despite the poll-strategy fix. Add a generic ' to: ' marker (with surrounding spaces) placed after the specific verb markers and before the bare 'password:' last resort. Matrix user ids and server names can't contain ' to: ', so it only ever anchors on the prose delimiter. Two regression tests cover the exact production wording. --- hive-c0re/src/matrix.rs | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/hive-c0re/src/matrix.rs b/hive-c0re/src/matrix.rs index a2750e88..30a40165 100644 --- a/hive-c0re/src/matrix.rs +++ b/hive-c0re/src/matrix.rs @@ -378,6 +378,14 @@ fn extract_new_password(bot_message: &str) -> Option { "reset to:", "set to: ", "set to:", + // Generic "… to: " form. tuwunel's actual reset-password reply is + // "Successfully reset password for @user:server to: " — the + // password follows " to: " but no recognised verb sits adjacent to it, + // so the markers above miss it. Matrix user ids / server names can't + // contain " to: ", so this only ever anchors on the prose delimiter. + // Placed after the specific verb markers and before the bare + // "password:" last resort. + " to: ", // Bare "new password:" without "is": "new password: ", "new password:", @@ -459,6 +467,23 @@ mod extract_new_password_tests { assert_eq!(extract_new_password(msg).as_deref(), Some("hunter2")); } + #[test] + fn tuwunel_reset_password_for_to_variant() { + // The actual tuwunel admin-room reply observed in production — the + // verb ("reset") is not adjacent to "to:", so only the generic + // " to: " marker catches it. + let msg = "Successfully reset password for @atlas:pr1ma.darkest.space to: N3wP@ssw0rd"; + assert_eq!(extract_new_password(msg).as_deref(), Some("N3wP@ssw0rd")); + } + + #[test] + fn to_marker_not_confused_by_user_id() { + // The user id contains no " to: " so the marker only fires on the + // real delimiter; the password is the token right after it. + let msg = "Successfully reset password for @sock:pr1ma.darkest.space to: abc123XYZ"; + assert_eq!(extract_new_password(msg).as_deref(), Some("abc123XYZ")); + } + #[test] fn bare_new_password_colon() { let msg = "New password: P@ssword1"; From 93d28ce0b166b07925796c76bc80d654f635ca8e Mon Sep 17 00:00:00 2001 From: atlas Date: Fri, 5 Jun 2026 00:44:42 +0200 Subject: [PATCH 2/2] fix: strip code-span backticks from extracted matrix password MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit mara observed the tuwunel reply renders the new password as a code span, so the plain message body carries literal backticks: Successfully reset password for @user:server to: `` The previous extraction stopped at the first whitespace, capturing the surrounding backticks ("``") and producing a login string that doesn't match the password the bot actually set — recovery would still fail after parsing. Strip a leading backtick after the marker and stop the token at the first whitespace OR closing backtick. Generated passwords contain neither, so a real password is never truncated mid-token. Two regression tests cover the backtick-wrapped form, including trailing prose after the closing backtick. --- hive-c0re/src/matrix.rs | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/hive-c0re/src/matrix.rs b/hive-c0re/src/matrix.rs index 30a40165..4fd5e1c9 100644 --- a/hive-c0re/src/matrix.rs +++ b/hive-c0re/src/matrix.rs @@ -395,10 +395,15 @@ fn extract_new_password(bot_message: &str) -> Option { ] { if let Some(pos) = lower.find(marker) { let rest = &bot_message[pos + marker.len()..]; - let rest = rest.trim_start(); - // Stop at first whitespace or newline; password must be non-empty. + // Strip leading whitespace, then a leading code-span backtick: + // tuwunel renders the password as a code span, so the plain + // `body` carries literal backticks ("… to: ``"). + let rest = rest.trim_start().trim_start_matches('`'); + // The password ends at the first whitespace OR the closing + // backtick. Generated passwords contain neither, so this never + // truncates a real password mid-token. let end = rest - .find(char::is_whitespace) + .find(|c: char| c.is_whitespace() || c == '`') .unwrap_or(rest.len()); let pw = rest[..end].trim(); if !pw.is_empty() { @@ -484,6 +489,20 @@ mod extract_new_password_tests { assert_eq!(extract_new_password(msg).as_deref(), Some("abc123XYZ")); } + #[test] + fn backtick_wrapped_password() { + // tuwunel renders the password as a code span; the plain body + // carries literal backticks. Strip them, don't capture them. + let msg = "Successfully reset password for @atlas:pr1ma.darkest.space to: `N3wP@ssw0rd`"; + assert_eq!(extract_new_password(msg).as_deref(), Some("N3wP@ssw0rd")); + } + + #[test] + fn backtick_wrapped_with_trailing_text() { + let msg = "Done. New password is: `hunter2` (store it now)"; + assert_eq!(extract_new_password(msg).as_deref(), Some("hunter2")); + } + #[test] fn bare_new_password_colon() { let msg = "New password: P@ssword1";