From fdd11030dc55a68142441ecf05f0bf33a8d38695 Mon Sep 17 00:00:00 2001 From: Gravirei Date: Mon, 20 Jul 2026 14:39:06 +0600 Subject: [PATCH 01/14] fix(gl): only report registered: false on 404 in whoami (#220) --- crates/gl/src/whoami.rs | 92 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 88 insertions(+), 4 deletions(-) diff --git a/crates/gl/src/whoami.rs b/crates/gl/src/whoami.rs index 66c1438c..ed5e0be0 100644 --- a/crates/gl/src/whoami.rs +++ b/crates/gl/src/whoami.rs @@ -1,6 +1,6 @@ //! `gl whoami` — print current identity and optional node registration info. -use anyhow::Result; +use anyhow::{bail, Result}; use clap::Args; use serde_json::{json, Value}; use std::path::PathBuf; @@ -51,11 +51,21 @@ pub async fn run(args: WhoamiArgs) -> Result<()> { } } } - Ok(_) => { + Ok(resp) if resp.status().as_u16() == 404 => { registered = Some(false); } - Err(_) => { - registered = Some(false); + Ok(resp) => { + let status = resp.status(); + let msg = resp + .json::() + .await + .ok() + .and_then(|v| v["message"].as_str().map(String::from)) + .unwrap_or_else(|| "request failed".to_string()); + bail!("agent lookup failed ({status}): {msg}"); + } + Err(e) => { + bail!("agent lookup failed: {e}"); } } } @@ -195,6 +205,80 @@ mod tests { run(args).await.unwrap(); } + #[tokio::test] + async fn test_whoami_with_node_forbidden() { + let dir = TempDir::new().unwrap(); + let kp = gitlawb_core::identity::Keypair::generate(); + let pem = kp.to_pem().unwrap(); + std::fs::write(dir.path().join("identity.pem"), pem.as_bytes()).unwrap(); + let did = kp.did().to_string(); + + let mut server = mockito::Server::new_async().await; + let _agent = server + .mock("GET", format!("/api/v1/agents/{did}").as_str()) + .with_status(403) + .with_header("content-type", "application/json") + .with_body(r#"{"message":"forbidden"}"#) + .create_async() + .await; + + let args = WhoamiArgs { + dir: Some(dir.path().to_path_buf()), + node: Some(server.url()), + json: false, + }; + let err = run(args).await.unwrap_err(); + let msg = format!("{err:?}"); + assert!(msg.contains("403"), "expected 403 error, got: {msg}"); + } + + #[tokio::test] + async fn test_whoami_with_node_server_error() { + let dir = TempDir::new().unwrap(); + let kp = gitlawb_core::identity::Keypair::generate(); + let pem = kp.to_pem().unwrap(); + std::fs::write(dir.path().join("identity.pem"), pem.as_bytes()).unwrap(); + let did = kp.did().to_string(); + + let mut server = mockito::Server::new_async().await; + let _agent = server + .mock("GET", format!("/api/v1/agents/{did}").as_str()) + .with_status(500) + .with_header("content-type", "application/json") + .with_body(r#"{"message":"internal error"}"#) + .create_async() + .await; + + let args = WhoamiArgs { + dir: Some(dir.path().to_path_buf()), + node: Some(server.url()), + json: false, + }; + let err = run(args).await.unwrap_err(); + let msg = format!("{err:?}"); + assert!(msg.contains("500"), "expected 500 error, got: {msg}"); + } + + #[tokio::test] + async fn test_whoami_with_node_transport_error() { + let dir = TempDir::new().unwrap(); + let kp = gitlawb_core::identity::Keypair::generate(); + let pem = kp.to_pem().unwrap(); + std::fs::write(dir.path().join("identity.pem"), pem.as_bytes()).unwrap(); + + let args = WhoamiArgs { + dir: Some(dir.path().to_path_buf()), + node: Some("http://127.0.0.1:1".to_string()), + json: false, + }; + let err = run(args).await.unwrap_err(); + let msg = format!("{err:?}"); + assert!( + msg.contains("agent lookup failed"), + "expected transport error, got: {msg}" + ); + } + #[tokio::test] async fn test_whoami_json_with_node() { let dir = TempDir::new().unwrap(); From 3add5356e36f0c2f89a0617fcb8018209bf722ff Mon Sep 17 00:00:00 2001 From: Gravirei Date: Wed, 22 Jul 2026 19:57:09 +0600 Subject: [PATCH 02/14] fix(gl): only report registered: false on 404 in whoami (#220) --- crates/gl/src/sync.rs | 4 +- crates/gl/src/whoami.rs | 95 +++++++++++++++++++++++++++++++++++++---- 2 files changed, 89 insertions(+), 10 deletions(-) diff --git a/crates/gl/src/sync.rs b/crates/gl/src/sync.rs index 634b1c0d..c17925ee 100644 --- a/crates/gl/src/sync.rs +++ b/crates/gl/src/sync.rs @@ -107,7 +107,7 @@ fn trigger_counts(resp: &serde_json::Value) -> (u64, u64) { /// Read at most `cap` bytes of a response body. Bounds the allocation from a /// hostile or broken node returning a huge error body — the display is capped /// separately, but the read itself must not be unbounded (INV-6, read half). -async fn read_body_capped(mut resp: reqwest::Response, cap: usize) -> String { +pub(crate) async fn read_body_capped(mut resp: reqwest::Response, cap: usize) -> String { let mut buf: Vec = Vec::new(); while buf.len() < cap { match resp.chunk().await { @@ -130,7 +130,7 @@ async fn read_body_capped(mut resp: reqwest::Response, cap: usize) -> String { /// reach the terminal verbatim (INV-6). We drop the C0/C1 control bytes (which /// defangs ANSI/OSC escapes) AND the Unicode bidi/format controls (which /// `char::is_control` does not cover — they can reorder the displayed line). -fn sanitize_node_msg(s: &str) -> String { +pub(crate) fn sanitize_node_msg(s: &str) -> String { s.chars() .filter(|c| !c.is_control() && !gitlawb_core::sanitize::is_bidi_format(*c)) .take(200) diff --git a/crates/gl/src/whoami.rs b/crates/gl/src/whoami.rs index ed5e0be0..e2e489b9 100644 --- a/crates/gl/src/whoami.rs +++ b/crates/gl/src/whoami.rs @@ -7,6 +7,7 @@ use std::path::PathBuf; use crate::http::NodeClient; use crate::identity::load_keypair_from_dir; +use crate::sync::{read_body_capped, sanitize_node_msg}; #[derive(Args)] pub struct WhoamiArgs { @@ -52,17 +53,27 @@ pub async fn run(args: WhoamiArgs) -> Result<()> { } } Ok(resp) if resp.status().as_u16() == 404 => { - registered = Some(false); + bail!( + "agent not found, or this node does not yet support the agents API (v0.3+)\n\ + upgrade the node or check GITLAWB_NODE is pointing to the right server" + ); } Ok(resp) => { let status = resp.status(); - let msg = resp - .json::() - .await + let raw = read_body_capped(resp, 8 * 1024).await; + let msg = serde_json::from_str::(&raw) .ok() - .and_then(|v| v["message"].as_str().map(String::from)) - .unwrap_or_else(|| "request failed".to_string()); - bail!("agent lookup failed ({status}): {msg}"); + .and_then(|v| { + v.get("message") + .or_else(|| v.get("error")) + .and_then(|m| m.as_str()) + .map(String::from) + }) + .unwrap_or(raw); + bail!( + "agent lookup failed ({status}): {}", + sanitize_node_msg(&msg) + ); } Err(e) => { bail!("agent lookup failed: {e}"); @@ -202,7 +213,12 @@ mod tests { node: Some(server.url()), json: false, }; - run(args).await.unwrap(); + let err = run(args).await.unwrap_err(); + let msg = format!("{err:?}"); + assert!( + msg.contains("agents API"), + "expected ambiguous 404 error, got: {msg}" + ); } #[tokio::test] @@ -279,6 +295,69 @@ mod tests { ); } + #[tokio::test] + async fn test_whoami_server_error_caps_body_size() { + let dir = TempDir::new().unwrap(); + let kp = gitlawb_core::identity::Keypair::generate(); + let pem = kp.to_pem().unwrap(); + std::fs::write(dir.path().join("identity.pem"), pem.as_bytes()).unwrap(); + let did = kp.did().to_string(); + + let mut server = mockito::Server::new_async().await; + let _agent = server + .mock("GET", format!("/api/v1/agents/{did}").as_str()) + .with_status(502) + .with_header("content-type", "application/json") + .with_body("x".repeat(100_000)) + .create_async() + .await; + + let args = WhoamiArgs { + dir: Some(dir.path().to_path_buf()), + node: Some(server.url()), + json: false, + }; + let err = run(args).await.unwrap_err(); + let msg = format!("{err:?}"); + assert!( + msg.contains("502"), + "expected 502 error with bounded body, got: {msg}" + ); + } + + #[tokio::test] + async fn test_whoami_server_error_sanitizes_controls() { + let dir = TempDir::new().unwrap(); + let kp = gitlawb_core::identity::Keypair::generate(); + let pem = kp.to_pem().unwrap(); + std::fs::write(dir.path().join("identity.pem"), pem.as_bytes()).unwrap(); + let did = kp.did().to_string(); + + let mut server = mockito::Server::new_async().await; + let _agent = server + .mock("GET", format!("/api/v1/agents/{did}").as_str()) + .with_status(500) + .with_header("content-type", "application/json") + .with_body("{\"message\":\"\\u{1b}[31mowned\\u{07}\\u{202e}evil\"}") + .create_async() + .await; + + let args = WhoamiArgs { + dir: Some(dir.path().to_path_buf()), + node: Some(server.url()), + json: false, + }; + let err = run(args).await.unwrap_err(); + let msg = format!("{err:?}"); + assert!( + msg.contains("owned"), + "expected sanitized error body, got: {msg}" + ); + assert!(!msg.contains('\u{1b}'), "ESC control char leaked: {msg}"); + assert!(!msg.contains('\u{07}'), "BEL control char leaked: {msg}"); + assert!(!msg.contains('\u{202e}'), "RTL override leaked: {msg}"); + } + #[tokio::test] async fn test_whoami_json_with_node() { let dir = TempDir::new().unwrap(); From 75b0df8f1c7bda6a02444ce17e11088b839a51b6 Mon Sep 17 00:00:00 2001 From: Gravirei Date: Wed, 22 Jul 2026 20:03:29 +0600 Subject: [PATCH 03/14] fix(whoami): update registered status handling on 404 response --- crates/gl/src/whoami.rs | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/crates/gl/src/whoami.rs b/crates/gl/src/whoami.rs index e2e489b9..c388c420 100644 --- a/crates/gl/src/whoami.rs +++ b/crates/gl/src/whoami.rs @@ -53,10 +53,7 @@ pub async fn run(args: WhoamiArgs) -> Result<()> { } } Ok(resp) if resp.status().as_u16() == 404 => { - bail!( - "agent not found, or this node does not yet support the agents API (v0.3+)\n\ - upgrade the node or check GITLAWB_NODE is pointing to the right server" - ); + registered = Some(false); } Ok(resp) => { let status = resp.status(); @@ -213,12 +210,7 @@ mod tests { node: Some(server.url()), json: false, }; - let err = run(args).await.unwrap_err(); - let msg = format!("{err:?}"); - assert!( - msg.contains("agents API"), - "expected ambiguous 404 error, got: {msg}" - ); + run(args).await.unwrap(); } #[tokio::test] @@ -246,6 +238,7 @@ mod tests { let err = run(args).await.unwrap_err(); let msg = format!("{err:?}"); assert!(msg.contains("403"), "expected 403 error, got: {msg}"); + assert!(msg.contains("forbidden"), "expected 'forbidden' in error, got: {msg}"); } #[tokio::test] @@ -273,6 +266,7 @@ mod tests { let err = run(args).await.unwrap_err(); let msg = format!("{err:?}"); assert!(msg.contains("500"), "expected 500 error, got: {msg}"); + assert!(msg.contains("internal error"), "expected 'internal error' in error, got: {msg}"); } #[tokio::test] From c10f2ca255d63331f6acf3a29629e2a1c11ff355 Mon Sep 17 00:00:00 2001 From: Gravirei Date: Wed, 22 Jul 2026 20:05:32 +0600 Subject: [PATCH 04/14] fix(gl): only report registered: false on 404 in whoami (#220) --- crates/gl/src/whoami.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/crates/gl/src/whoami.rs b/crates/gl/src/whoami.rs index c388c420..683dc372 100644 --- a/crates/gl/src/whoami.rs +++ b/crates/gl/src/whoami.rs @@ -238,7 +238,10 @@ mod tests { let err = run(args).await.unwrap_err(); let msg = format!("{err:?}"); assert!(msg.contains("403"), "expected 403 error, got: {msg}"); - assert!(msg.contains("forbidden"), "expected 'forbidden' in error, got: {msg}"); + assert!( + msg.contains("forbidden"), + "expected 'forbidden' in error, got: {msg}" + ); } #[tokio::test] @@ -266,7 +269,10 @@ mod tests { let err = run(args).await.unwrap_err(); let msg = format!("{err:?}"); assert!(msg.contains("500"), "expected 500 error, got: {msg}"); - assert!(msg.contains("internal error"), "expected 'internal error' in error, got: {msg}"); + assert!( + msg.contains("internal error"), + "expected 'internal error' in error, got: {msg}" + ); } #[tokio::test] From 10e34928254205fafa8e96513a3df7899bfb10b1 Mon Sep 17 00:00:00 2001 From: Gravirei Date: Thu, 23 Jul 2026 11:38:32 +0600 Subject: [PATCH 05/14] fix(gl): only report registered: false on 404 in whoami (#220) --- crates/gl/src/whoami.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/crates/gl/src/whoami.rs b/crates/gl/src/whoami.rs index 683dc372..341c672f 100644 --- a/crates/gl/src/whoami.rs +++ b/crates/gl/src/whoami.rs @@ -53,7 +53,10 @@ pub async fn run(args: WhoamiArgs) -> Result<()> { } } Ok(resp) if resp.status().as_u16() == 404 => { - registered = Some(false); + bail!( + "agent not found, or this node does not yet support the agents API (v0.3+)\n\ + upgrade the node or check GITLAWB_NODE is pointing to the right server" + ); } Ok(resp) => { let status = resp.status(); @@ -210,7 +213,12 @@ mod tests { node: Some(server.url()), json: false, }; - run(args).await.unwrap(); + let err = run(args).await.unwrap_err(); + let msg = format!("{err:?}"); + assert!( + msg.contains("agents API"), + "expected ambiguous 404 error, got: {msg}" + ); } #[tokio::test] From 12bc759ea6b20ab97c9dfeb4e00d7c6a6ca4fcfe Mon Sep 17 00:00:00 2001 From: Gravirei Date: Fri, 24 Jul 2026 12:33:47 +0600 Subject: [PATCH 06/14] fix(gl): only report registered: false on 404 in whoami (#220) --- crates/gl/src/whoami.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crates/gl/src/whoami.rs b/crates/gl/src/whoami.rs index 341c672f..6e35827b 100644 --- a/crates/gl/src/whoami.rs +++ b/crates/gl/src/whoami.rs @@ -331,6 +331,11 @@ mod tests { msg.contains("502"), "expected 502 error with bounded body, got: {msg}" ); + assert!( + msg.len() < 1000, + "error message too long ({} bytes) — body was not capped", + msg.len() + ); } #[tokio::test] From b0e5cc0ee0e91ebe35689bb1e073fa7e48ab37ee Mon Sep 17 00:00:00 2001 From: Gravirei Date: Fri, 24 Jul 2026 12:37:21 +0600 Subject: [PATCH 07/14] fix(gl): only report registered: false on 404 in whoami (#220) --- crates/gl/src/whoami.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/gl/src/whoami.rs b/crates/gl/src/whoami.rs index 6e35827b..c7d36117 100644 --- a/crates/gl/src/whoami.rs +++ b/crates/gl/src/whoami.rs @@ -331,10 +331,11 @@ mod tests { msg.contains("502"), "expected 502 error with bounded body, got: {msg}" ); + let display = format!("{err}"); assert!( - msg.len() < 1000, + display.len() < 1000, "error message too long ({} bytes) — body was not capped", - msg.len() + display.len() ); } From 3f41df5868838544876473c2560c3d6be20e679c Mon Sep 17 00:00:00 2001 From: Gravirei Date: Fri, 24 Jul 2026 19:09:02 +0600 Subject: [PATCH 08/14] fix(gl): only report registered: false on 404 in whoami (#220) --- crates/gl/src/whoami.rs | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/crates/gl/src/whoami.rs b/crates/gl/src/whoami.rs index c7d36117..16720d7c 100644 --- a/crates/gl/src/whoami.rs +++ b/crates/gl/src/whoami.rs @@ -53,10 +53,7 @@ pub async fn run(args: WhoamiArgs) -> Result<()> { } } Ok(resp) if resp.status().as_u16() == 404 => { - bail!( - "agent not found, or this node does not yet support the agents API (v0.3+)\n\ - upgrade the node or check GITLAWB_NODE is pointing to the right server" - ); + registered = Some(false); } Ok(resp) => { let status = resp.status(); @@ -213,12 +210,7 @@ mod tests { node: Some(server.url()), json: false, }; - let err = run(args).await.unwrap_err(); - let msg = format!("{err:?}"); - assert!( - msg.contains("agents API"), - "expected ambiguous 404 error, got: {msg}" - ); + run(args).await.unwrap(); } #[tokio::test] From 16cc3b37e8268d048f3145ab16978dae297df5d4 Mon Sep 17 00:00:00 2001 From: Gravirei Date: Fri, 24 Jul 2026 19:26:52 +0600 Subject: [PATCH 09/14] fix(gl): only report registered: false on 404 in whoami (#220) --- crates/gl/src/whoami.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/gl/src/whoami.rs b/crates/gl/src/whoami.rs index 16720d7c..1407b134 100644 --- a/crates/gl/src/whoami.rs +++ b/crates/gl/src/whoami.rs @@ -344,7 +344,7 @@ mod tests { .mock("GET", format!("/api/v1/agents/{did}").as_str()) .with_status(500) .with_header("content-type", "application/json") - .with_body("{\"message\":\"\\u{1b}[31mowned\\u{07}\\u{202e}evil\"}") + .with_body("{\"message\":\"\\u001b[31mowned\\u0007\\u202eevil\"}") .create_async() .await; From 3cc15cf7247396ab1efcd279d3dd51dd8deaf014 Mon Sep 17 00:00:00 2001 From: Gravirei Date: Sun, 26 Jul 2026 02:27:49 +0600 Subject: [PATCH 10/14] fix(gl): only report registered: false on 404 in whoami (#220) --- crates/gl/src/whoami.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/gl/src/whoami.rs b/crates/gl/src/whoami.rs index 1407b134..be0ee263 100644 --- a/crates/gl/src/whoami.rs +++ b/crates/gl/src/whoami.rs @@ -42,7 +42,7 @@ pub async fn run(args: WhoamiArgs) -> Result<()> { if let Some(caps) = info["capabilities"].as_array() { capabilities = caps .iter() - .filter_map(|c| c.as_str().map(String::from)) + .filter_map(|c| c.as_str().map(sanitize_node_msg)) .collect(); } // Try to get repo count From fd78cfa3ad0e7997b284371a55b7fb52753236d1 Mon Sep 17 00:00:00 2001 From: Gravirei Date: Mon, 27 Jul 2026 12:41:41 +0600 Subject: [PATCH 11/14] fix(gl): only report registered: false on 404 in whoami (#220) --- crates/gl/src/whoami.rs | 82 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 75 insertions(+), 7 deletions(-) diff --git a/crates/gl/src/whoami.rs b/crates/gl/src/whoami.rs index be0ee263..483a5170 100644 --- a/crates/gl/src/whoami.rs +++ b/crates/gl/src/whoami.rs @@ -23,6 +23,11 @@ pub struct WhoamiArgs { } pub async fn run(args: WhoamiArgs) -> Result<()> { + let mut stdout = std::io::stdout().lock(); + run_to_writer(args, &mut stdout).await +} + +pub(crate) async fn run_to_writer(args: WhoamiArgs, w: &mut impl std::io::Write) -> Result<()> { let keypair = load_keypair_from_dir(args.dir.as_deref())?; let did = keypair.did().to_string(); let short = did.split(':').next_back().unwrap_or(&did).to_string(); @@ -95,21 +100,21 @@ pub async fn run(args: WhoamiArgs) -> Result<()> { if let Some(rc) = repo_count { out["repos"] = json!(rc); } - println!("{}", serde_json::to_string_pretty(&out)?); + writeln!(w, "{}", serde_json::to_string_pretty(&out)?)?; } else { - println!("DID: {did}"); - println!("Short: {short}"); + writeln!(w, "DID: {did}")?; + writeln!(w, "Short: {short}")?; if let Some(reg) = registered { - println!("Registered: {}", if reg { "yes" } else { "no" }); + writeln!(w, "Registered: {}", if reg { "yes" } else { "no" })?; } if let Some(ts) = trust_score { - println!("Trust: {ts:.2}"); + writeln!(w, "Trust: {ts:.2}")?; } if !capabilities.is_empty() { - println!("Caps: {}", capabilities.join(", ")); + writeln!(w, "Caps: {}", capabilities.join(", "))?; } if let Some(rc) = repo_count { - println!("Repos: {rc}"); + writeln!(w, "Repos: {rc}")?; } } Ok(()) @@ -399,4 +404,67 @@ mod tests { }; run(args).await.unwrap(); } + + #[tokio::test] + async fn test_whoami_sanitizes_hostile_capabilities() { + let dir = TempDir::new().unwrap(); + let kp = gitlawb_core::identity::Keypair::generate(); + let pem = kp.to_pem().unwrap(); + std::fs::write(dir.path().join("identity.pem"), pem.as_bytes()).unwrap(); + let did = kp.did().to_string(); + let short = did.split(':').next_back().unwrap().to_string(); + + let mut server = mockito::Server::new_async().await; + let _agent = server + .mock("GET", format!("/api/v1/agents/{did}").as_str()) + .with_status(200) + .with_header("content-type", "application/json") + .with_body( + "{\"trust_score\":0.5,\"capabilities\":[\"\\u001b]0;PWNED\\u0007repo:write\",\"\\u202egnitirw-tfel\"]}", + ) + .create_async() + .await; + let _repos = server + .mock( + "GET", + mockito::Matcher::Regex(format!(r"^/api/v1/repos\?owner={short}")), + ) + .with_status(200) + .with_header("content-type", "application/json") + .with_body("[]") + .create_async() + .await; + + // Human mode: no control bytes or bidi overrides reach the terminal + let mut buf = Vec::new(); + let args = WhoamiArgs { + dir: Some(dir.path().to_path_buf()), + node: Some(server.url()), + json: false, + }; + run_to_writer(args, &mut buf).await.unwrap(); + let out = String::from_utf8(buf).unwrap(); + assert!(!out.contains('\u{1b}'), "ESC leaked in human mode: {out:?}"); + assert!(!out.contains('\u{07}'), "BEL leaked in human mode: {out:?}"); + assert!( + !out.contains('\u{202e}'), + "RLO leaked in human mode: {out:?}" + ); + assert!(out.contains("repo:write"), "benign text missing: {out:?}"); + assert!(out.contains("tfel"), "reversed text missing: {out:?}"); + + // JSON mode: serde escapes C0 but passes bidi — ensure no U+202E + let mut buf = Vec::new(); + let args = WhoamiArgs { + dir: Some(dir.path().to_path_buf()), + node: Some(server.url()), + json: true, + }; + run_to_writer(args, &mut buf).await.unwrap(); + let out = String::from_utf8(buf).unwrap(); + assert!( + !out.contains('\u{202e}'), + "RLO leaked in JSON mode: {out:?}" + ); + } } From a3291741757038307e61d3952e6c20aa4901c8a3 Mon Sep 17 00:00:00 2001 From: Gravirei Date: Thu, 6 Aug 2026 11:57:23 +0600 Subject: [PATCH 12/14] fix(gl): assert 404 verdict and harden transport error handling in whoami (#220) --- crates/gl/src/whoami.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/gl/src/whoami.rs b/crates/gl/src/whoami.rs index 483a5170..e22b1b97 100644 --- a/crates/gl/src/whoami.rs +++ b/crates/gl/src/whoami.rs @@ -301,7 +301,7 @@ mod tests { } #[tokio::test] - async fn test_whoami_server_error_caps_body_size() { + async fn test_whoami_server_error_body_display_bounded() { let dir = TempDir::new().unwrap(); let kp = gitlawb_core::identity::Keypair::generate(); let pem = kp.to_pem().unwrap(); From b4007a5e005c63aa0c083b969cd8b5f65b918adb Mon Sep 17 00:00:00 2001 From: Gravirei Date: Thu, 6 Aug 2026 11:58:11 +0600 Subject: [PATCH 13/14] fix(gl): assert whoami 404 verdict and preserve transport error cause (#220) --- crates/gl/src/whoami.rs | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/crates/gl/src/whoami.rs b/crates/gl/src/whoami.rs index e22b1b97..13ee77c6 100644 --- a/crates/gl/src/whoami.rs +++ b/crates/gl/src/whoami.rs @@ -1,6 +1,6 @@ //! `gl whoami` — print current identity and optional node registration info. -use anyhow::{bail, Result}; +use anyhow::{bail, Context, Result}; use clap::Args; use serde_json::{json, Value}; use std::path::PathBuf; @@ -23,8 +23,7 @@ pub struct WhoamiArgs { } pub async fn run(args: WhoamiArgs) -> Result<()> { - let mut stdout = std::io::stdout().lock(); - run_to_writer(args, &mut stdout).await + run_to_writer(args, &mut std::io::stdout()).await } pub(crate) async fn run_to_writer(args: WhoamiArgs, w: &mut impl std::io::Write) -> Result<()> { @@ -78,7 +77,8 @@ pub(crate) async fn run_to_writer(args: WhoamiArgs, w: &mut impl std::io::Write) ); } Err(e) => { - bail!("agent lookup failed: {e}"); + let msg = sanitize_node_msg(&e.to_string()); + return Err(e).context(format!("agent lookup failed: {msg}")); } } } @@ -215,7 +215,23 @@ mod tests { node: Some(server.url()), json: false, }; - run(args).await.unwrap(); + let mut out = Vec::new(); + run_to_writer(args, &mut out).await.unwrap(); + let out = String::from_utf8(out).unwrap(); + assert!(out.contains("Registered: no"), "unexpected output: {out}"); + + let args = WhoamiArgs { + dir: Some(dir.path().to_path_buf()), + node: Some(server.url()), + json: true, + }; + let mut out = Vec::new(); + run_to_writer(args, &mut out).await.unwrap(); + let out = String::from_utf8(out).unwrap(); + assert!( + out.contains("\"registered\": false"), + "unexpected output: {out}" + ); } #[tokio::test] From a27a2678712cc512b316f143d8c053dbdf981a4b Mon Sep 17 00:00:00 2001 From: Gravirei Date: Thu, 6 Aug 2026 13:18:29 +0600 Subject: [PATCH 14/14] fix(gl): require non-empty error message and use ephemeral port in whoami tests (#220) --- crates/gl/src/whoami.rs | 46 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 41 insertions(+), 5 deletions(-) diff --git a/crates/gl/src/whoami.rs b/crates/gl/src/whoami.rs index 13ee77c6..7f608096 100644 --- a/crates/gl/src/whoami.rs +++ b/crates/gl/src/whoami.rs @@ -65,10 +65,12 @@ pub(crate) async fn run_to_writer(args: WhoamiArgs, w: &mut impl std::io::Write) let msg = serde_json::from_str::(&raw) .ok() .and_then(|v| { - v.get("message") - .or_else(|| v.get("error")) - .and_then(|m| m.as_str()) - .map(String::from) + let non_empty = |m: Option<&Value>| { + m.and_then(|m| m.as_str()) + .map(String::from) + .filter(|s| !s.is_empty()) + }; + non_empty(v.get("message")).or_else(|| non_empty(v.get("error"))) }) .unwrap_or(raw); bail!( @@ -296,6 +298,36 @@ mod tests { ); } + #[tokio::test] + async fn test_whoami_server_error_unusable_message_uses_error() { + let dir = TempDir::new().unwrap(); + let kp = gitlawb_core::identity::Keypair::generate(); + let pem = kp.to_pem().unwrap(); + std::fs::write(dir.path().join("identity.pem"), pem.as_bytes()).unwrap(); + let did = kp.did().to_string(); + + let mut server = mockito::Server::new_async().await; + let _agent = server + .mock("GET", format!("/api/v1/agents/{did}").as_str()) + .with_status(500) + .with_header("content-type", "application/json") + .with_body(r#"{"message":"","error":"boom"}"#) + .create_async() + .await; + + let args = WhoamiArgs { + dir: Some(dir.path().to_path_buf()), + node: Some(server.url()), + json: false, + }; + let err = run(args).await.unwrap_err(); + let msg = format!("{err:?}"); + assert!( + msg.contains("boom"), + "expected 'boom' from error field, got: {msg}" + ); + } + #[tokio::test] async fn test_whoami_with_node_transport_error() { let dir = TempDir::new().unwrap(); @@ -303,9 +335,13 @@ mod tests { let pem = kp.to_pem().unwrap(); std::fs::write(dir.path().join("identity.pem"), pem.as_bytes()).unwrap(); + let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap(); + let node = format!("http://{}", listener.local_addr().unwrap()); + drop(listener); + let args = WhoamiArgs { dir: Some(dir.path().to_path_buf()), - node: Some("http://127.0.0.1:1".to_string()), + node: Some(node), json: false, }; let err = run(args).await.unwrap_err();