diff --git a/crates/pm-core/src/config.rs b/crates/pm-core/src/config.rs index 153182b..bee8dec 100644 --- a/crates/pm-core/src/config.rs +++ b/crates/pm-core/src/config.rs @@ -169,9 +169,7 @@ pub fn derive_allowed_origins(sso_callback_url: &str) -> Vec { return vec![]; } // Authority is everything up to the first `/`, `?`, or `#`. - let authority_end = rest - .find(['/', '?', '#']) - .unwrap_or(rest.len()); + let authority_end = rest.find(['/', '?', '#']).unwrap_or(rest.len()); let authority = &rest[..authority_end]; if authority.is_empty() { return vec![]; diff --git a/crates/pm-web/src/routes/ws.rs b/crates/pm-web/src/routes/ws.rs index 720ed04..795f97e 100644 --- a/crates/pm-web/src/routes/ws.rs +++ b/crates/pm-web/src/routes/ws.rs @@ -101,9 +101,7 @@ fn parse_origin_header(value: &str) -> Option { return None; } // Authority is everything up to the first `/`, `?`, or `#`. - let authority_end = rest - .find(['/', '?', '#']) - .unwrap_or(rest.len()); + let authority_end = rest.find(['/', '?', '#']).unwrap_or(rest.len()); let authority = &rest[..authority_end]; if authority.is_empty() { return None; @@ -145,12 +143,12 @@ fn is_origin_allowed(origin: &Origin, allowlist: &[String]) -> bool { return false; } let incoming = origin.canonical(); - allowlist.iter().any(|entry| { - match parse_origin_header(entry) { + allowlist + .iter() + .any(|entry| match parse_origin_header(entry) { Some(parsed) => parsed.canonical() == incoming, None => false, - } - }) + }) } /// Read the `Origin` header from a request and check it against the @@ -172,7 +170,7 @@ fn check_origin( ), "missing", )); - } + }, }; let raw_str = match raw.to_str() { Ok(s) => s, @@ -185,7 +183,7 @@ fn check_origin( ), "non-ascii", )); - } + }, }; let origin = match parse_origin_header(raw_str) { Some(o) => o, @@ -198,7 +196,7 @@ fn check_origin( ), "malformed", )); - } + }, }; if !is_origin_allowed(&origin, allowlist) { return Err(( @@ -425,7 +423,9 @@ mod tests { #[test] fn parse_lowercases_scheme() { assert_eq!( - parse_origin_header("HTTPS://App.Example.com").unwrap().scheme, + parse_origin_header("HTTPS://App.Example.com") + .unwrap() + .scheme, "https" ); } @@ -546,7 +546,10 @@ mod tests { #[test] fn allowed_default_port_normalization_allowlist() { let o = parse_origin_header("https://app.example.com").unwrap(); - assert!(is_origin_allowed(&o, &["https://app.example.com:443".into()])); + assert!(is_origin_allowed( + &o, + &["https://app.example.com:443".into()] + )); } #[test] @@ -619,7 +622,10 @@ mod tests { #[test] fn check_rejects_disallowed_origin() { let mut h = HeaderMap::new(); - h.insert(axum::http::header::ORIGIN, "https://evil.example".parse().unwrap()); + h.insert( + axum::http::header::ORIGIN, + "https://evil.example".parse().unwrap(), + ); let err = check_origin(&h, &["https://app.example.com".into()]).unwrap_err(); assert_eq!(err.0 .0, StatusCode::FORBIDDEN); assert_eq!(err.1, "not-allowlisted"); @@ -628,7 +634,10 @@ mod tests { #[test] fn check_rejects_empty_allowlist() { let mut h = HeaderMap::new(); - h.insert(axum::http::header::ORIGIN, "https://app.example.com".parse().unwrap()); + h.insert( + axum::http::header::ORIGIN, + "https://app.example.com".parse().unwrap(), + ); let err = check_origin(&h, &[]).unwrap_err(); assert_eq!(err.0 .0, StatusCode::FORBIDDEN); assert_eq!(err.1, "not-allowlisted"); @@ -637,7 +646,10 @@ mod tests { #[test] fn check_allows_valid_origin() { let mut h = HeaderMap::new(); - h.insert(axum::http::header::ORIGIN, "https://app.example.com".parse().unwrap()); + h.insert( + axum::http::header::ORIGIN, + "https://app.example.com".parse().unwrap(), + ); assert!(check_origin(&h, &["https://app.example.com".into()]).is_ok()); } @@ -654,7 +666,10 @@ mod tests { #[test] fn check_allows_case_insensitive_host() { let mut h = HeaderMap::new(); - h.insert(axum::http::header::ORIGIN, "https://App.Example.com".parse().unwrap()); + h.insert( + axum::http::header::ORIGIN, + "https://App.Example.com".parse().unwrap(), + ); assert!(check_origin(&h, &["https://app.example.com".into()]).is_ok()); } } diff --git a/tasks/lessons.md b/tasks/lessons.md index e109c3a..18f5c23 100644 --- a/tasks/lessons.md +++ b/tasks/lessons.md @@ -15,6 +15,21 @@ **Rule:** Check the obvious source (gitea repo, Vaultwarden store) before spinning wheels on complex alternatives. **Status:** Active +## 2026-06-02: SSH_ASKPASS=/dev/null Blocks Git Commit Signing +**Pattern:** The container environment sets `SSH_ASKPASS=/dev/null` and `SSH_ASKPASS_REQUIRE=force`, which overrides ssh-agent and prevents git from finding signing keys during commit signing. +**Mistake:** Attempted git commit multiple times without checking why it hung. The signing key was in ssh-agent but SSH_ASKPASS was redirecting the passphrase prompt to /dev/null (not executable), causing the commit to fail with "incorrect passphrase". +**Fix:** Unset `SSH_ASKPASS` and `SSH_ASKPASS_REQUIRE` before running git commit, then use `ssh-add` with the passphrase from Vaultwarden to add the signing key to ssh-agent. +**Rule:** Before git commit signing, check `echo $SSH_ASKPASS` and `echo $SSH_ASKPASS_REQUIRE`. If SSH_ASKPASS is set to /dev/null or another non-executable, unset both variables before committing. +**Rule:** Always retrieve signing key passphrases from Vaultwarden using `vw_client.py get`, not from local files or memory. +**Status:** Active + +## 2026-06-02: Always Run credential-bootstrap at Session Start +**Pattern:** Profile rules mandate running `bash /a0/usr/skills/credential-bootstrap/scripts/bootstrap.sh` at the start of every conversation before any SSH or authenticated operations. I violated this rule by starting work without bootstrapping. +**Mistake:** Began implementation work without running credential-bootstrap, then wasted multiple attempts trying to commit with a signing key that wasn't in ssh-agent. +**Rule:** ALWAYS run credential-bootstrap at session start, before any authenticated operations. This includes git commit signing. +**Rule:** If a credential operation fails, STOP and run credential-bootstrap before retrying. Do not attempt workarounds. +**Status:** Active + ## 2026-05-08: Vaultwarden Is the Source of Truth for All Credentials **Pattern:** SSH keys in ~/.ssh/ are ephemeral — lost on every container recreation. Local copies are unreliable. **Rule:** ALWAYS pull credentials (SSH keys, API tokens, passwords) from Vaultwarden when needed. Do NOT rely on local copies in ~/.ssh/ or /a0/usr/storage/ as they may be stale or missing after container recreation.