fix(rpc): let the PASSWORD_REQUIRED sentinel through the error sanitizer

No node in the fleet could mint a Trusted federation invite or promote a
peer to Trusted from the dashboard. The cause is one case comparison.

Granting Trusted requires the node password; when none is supplied the
handler bails with "PASSWORD_REQUIRED: node password required to grant
Trusted" — a sentinel the frontend machine-reads (isPasswordRequired
checks includes('PASSWORD_REQUIRED')) to know it should open the password
prompt. sanitize_error_message allowlists "Password", but starts_with is
case-sensitive, so the ALL-CAPS sentinel failed the allowlist and
collapsed into "Operation failed. Check server logs for details." The
frontend never saw the sentinel, the prompt never opened, and the flow
died with a generic error on every node.

Reproduced via direct RPC: federation.invite {trust_level:"trusted"}
without a password returned the generic message; the same call WITH the
password minted a code fine — the machinery was healthy, only the signal
was being eaten.

"Tor address not available. Tor may not be running." — the invite
handler's other user-actionable precondition — was masked the same way
and is allowlisted too.

Regression tests pin both passing through verbatim, and
internal_errors_stay_generic still passes, so the generic masking of real
internals is unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
archipelago
2026-08-09 07:49:56 -04:00
co-authored by Claude Fable 5
parent 7ec4bdb22d
commit e5a8fce198
@@ -58,6 +58,18 @@ pub(super) fn sanitize_error_message(msg: &str) -> String {
"must be",
"cannot",
"Password",
// The federation escalation sentinel. "Password" above does NOT cover
// it — starts_with is case-sensitive and the sentinel is ALL-CAPS —
// so the frontend's isPasswordRequired() never saw it and the
// password prompt could never open. Net effect: no node could mint a
// Trusted invite or promote a peer from the trust dropdown, fleet-wide
// (2026-08-09). The sentinel is machine-read by the UI; it must pass
// through verbatim.
"PASSWORD_REQUIRED",
// "Tor address not available. Tor may not be running." — the invite
// handler's precondition, entirely user-actionable, was likewise
// collapsing into the generic message.
"Tor address not available",
"Session",
"Failed to pull",
"Failed to start",
@@ -171,6 +183,24 @@ pub(super) fn sanitize_error_message(msg: &str) -> String {
mod sanitize_tests {
use super::sanitize_error_message;
#[test]
fn password_required_sentinel_passes_through_verbatim() {
// The UI machine-reads this sentinel (isPasswordRequired checks
// `includes('PASSWORD_REQUIRED')`) to know it should open the password
// prompt. The "Password" prefix does not cover it — starts_with is
// case-sensitive — and masking it made Trusted invites and trust
// promotion impossible on EVERY node (2026-08-09): the prompt simply
// never opened.
let msg = "PASSWORD_REQUIRED: node password required to grant Trusted";
assert_eq!(sanitize_error_message(msg), msg);
}
#[test]
fn tor_unavailable_precondition_passes_through() {
let msg = "Tor address not available. Tor may not be running.";
assert_eq!(sanitize_error_message(msg), msg);
}
#[test]
fn seed_reveal_errors_pass_through() {
// Every user-actionable seed.reveal failure must reach the user —