From b88994d1cbedf5be7f68f221ee0e0a1c08deff0c Mon Sep 17 00:00:00 2001 From: "Atlas (Agentbot Ops)" Date: Tue, 19 May 2026 14:08:59 +0100 Subject: [PATCH 1/2] Resolve bare DID aliases for trust and cert lookups Public profile and CLI surfaces accept bare z6Mk keys, but trust and certificate data are stored against full did:key identities. Normalize agent profile lookups and make cert commands resolve owner-less repos against the caller DID so valid pushes are not hidden behind node-owned paths. Constraint: Public URLs and CLI examples commonly use short z6Mk identifiers Constraint: Ref certificates are issued after push and stored under the owner repo id Rejected: Register duplicate bare-key identities | would split trust and certificate history across aliases Rejected: Require full certificate UUIDs in show output | list already displays short IDs as the human-facing handle Confidence: high Scope-risk: narrow Directive: Keep bare key inputs as aliases for did:key, not separate identities Tested: cargo check -p gl Tested: cargo run -q -p gl -- cert list agentbot-opensource Tested: cargo run -q -p gl -- cert show agentbot-opensource 3686b1fe Tested: cargo check -p gitlawb-node Tested: cargo test -p gitlawb-node normalize_agent_did --- crates/gitlawb-node/src/api/agents.rs | 37 +++++++++++++--- crates/gl/src/cert.rs | 63 +++++++++++++++++++++------ 2 files changed, 82 insertions(+), 18 deletions(-) diff --git a/crates/gitlawb-node/src/api/agents.rs b/crates/gitlawb-node/src/api/agents.rs index 39e7063..1c3eef9 100644 --- a/crates/gitlawb-node/src/api/agents.rs +++ b/crates/gitlawb-node/src/api/agents.rs @@ -8,6 +8,14 @@ use serde::{Deserialize, Serialize}; use crate::error::{AppError, Result}; use crate::state::AppState; +fn normalize_agent_did(did: &str) -> String { + if did.starts_with("did:") { + did.to_string() + } else { + format!("did:key:{did}") + } +} + #[derive(Debug, Serialize)] pub struct TrustResponse { pub did: String, @@ -66,11 +74,12 @@ pub async fn show_agent( State(state): State, Path(did): Path, ) -> Result<(StatusCode, Json)> { + let normalized_did = normalize_agent_did(&did); let agent = state .db - .get_agent(&did) + .get_agent(&normalized_did) .await? - .ok_or_else(|| AppError::NotFound(format!("agent {did} not found")))?; + .ok_or_else(|| AppError::NotFound(format!("agent {normalized_did} not found")))?; Ok(( StatusCode::OK, Json(AgentResponse { @@ -88,14 +97,32 @@ pub async fn get_trust( State(state): State, Path(did): Path, ) -> Result> { - let trust_score = state.db.get_trust_score(&did).await?; - let push_count = state.db.get_push_count(&did).await?; + let normalized_did = normalize_agent_did(&did); + let trust_score = state.db.get_trust_score(&normalized_did).await?; + let push_count = state.db.get_push_count(&normalized_did).await?; let level = trust_level(trust_score); Ok(Json(TrustResponse { - did, + did: normalized_did, trust_score, push_count, level, })) } + +#[cfg(test)] +mod tests { + use super::normalize_agent_did; + + #[test] + fn normalize_agent_did_preserves_full_did() { + let did = "did:key:z6MkExample"; + + assert_eq!(normalize_agent_did(did), did); + } + + #[test] + fn normalize_agent_did_expands_bare_key() { + assert_eq!(normalize_agent_did("z6MkExample"), "did:key:z6MkExample"); + } +} diff --git a/crates/gl/src/cert.rs b/crates/gl/src/cert.rs index be51e1b..7940252 100644 --- a/crates/gl/src/cert.rs +++ b/crates/gl/src/cert.rs @@ -7,6 +7,7 @@ use clap::{Args, Subcommand}; use serde_json::Value; use crate::http::NodeClient; +use crate::identity::load_keypair_from_dir; #[derive(Args)] pub struct CertArgs { @@ -41,20 +42,25 @@ pub async fn run(args: CertArgs) -> Result<()> { } } -/// Resolve "repo" into (owner, name) — if no slash, use the node's own DID short form. +/// Resolve "repo" into (owner, name) using the caller's DID when no slash is given. async fn resolve_repo(repo: &str, node: &str) -> Result<(String, String)> { if let Some((owner, name)) = repo.split_once('/') { Ok((owner.to_string(), name.to_string())) } else { - let client = NodeClient::new(node, None); - let info: Value = client - .get("/") - .await? - .json() - .await - .context("failed to fetch node info")?; - let did = info["did"].as_str().context("node info missing 'did'")?; - let short = did.split(':').next_back().unwrap_or(did).to_string(); + let short = if let Ok(kp) = load_keypair_from_dir(None) { + let did = kp.did().to_string(); + did.split(':').next_back().unwrap_or(&did).to_string() + } else { + let client = NodeClient::new(node, None); + let info: Value = client + .get("/") + .await? + .json() + .await + .context("failed to fetch node info")?; + let did = info["did"].as_str().context("node info missing 'did'")?; + did.split(':').next_back().unwrap_or(did).to_string() + }; Ok((short, repo.to_string())) } } @@ -94,15 +100,16 @@ async fn cmd_show(repo: String, id: String, node: String) -> Result<()> { let (owner, name) = resolve_repo(&repo, &node).await?; let client = NodeClient::new(&node, None); + let id = resolve_cert_id(&client, &owner, &name, &id).await?; // Fetch the certificate let path = format!("/api/v1/repos/{owner}/{name}/certs/{id}"); - let cert: Value = client + let resp = client .get(&path) .await? - .json() - .await + .error_for_status() .context("certificate not found")?; + let cert: Value = resp.json().await.context("certificate not found")?; let cert_id = cert["id"].as_str().unwrap_or("?"); let ref_name = cert["ref_name"].as_str().unwrap_or("?"); @@ -155,3 +162,33 @@ async fn cmd_show(repo: String, id: String, node: String) -> Result<()> { Ok(()) } + +async fn resolve_cert_id(client: &NodeClient, owner: &str, name: &str, id: &str) -> Result { + if id.len() >= 36 { + return Ok(id.to_string()); + } + + let path = format!("/api/v1/repos/{owner}/{name}/certs"); + let resp: Value = client + .get(&path) + .await? + .error_for_status() + .context("failed to list certificates")? + .json() + .await + .context("failed to list certificates")?; + + let certs = resp["certificates"].as_array().cloned().unwrap_or_default(); + let matches: Vec = certs + .iter() + .filter_map(|cert| cert["id"].as_str()) + .filter(|cert_id| cert_id.starts_with(id)) + .map(ToString::to_string) + .collect(); + + match matches.as_slice() { + [full_id] => Ok(full_id.to_string()), + [] => Ok(id.to_string()), + _ => anyhow::bail!("certificate prefix {id} matches multiple certificates"), + } +} From 437af97ffcf55e3b0fb10582946910df298b4087 Mon Sep 17 00:00:00 2001 From: "Atlas (Agentbot Ops)" Date: Thu, 21 May 2026 05:44:50 +0100 Subject: [PATCH 2/2] Preserve agent rank on short profile URLs The public profile route can use compact handles like z6MkpUq1, while trust history is stored under the full did:key identity. Resolve exact aliases first, then fall back to matching registered agent key prefixes so older high-level identities are not displayed as fresh newcomers. Constraint: Public GitLawb profile URLs expose short z6Mk handles. Constraint: Trust and push history remain keyed by canonical did:key identities. Rejected: Mint duplicate short-handle agent rows | would split rank and push history across aliases. Confidence: high Scope-risk: narrow Directive: Keep short profile handles as aliases for canonical did:key identities, never as separate trust principals. Tested: cargo fmt --check -p gitlawb-node Tested: cargo test -p gitlawb-node agent Tested: cargo check -p gitlawb-node Tested: cargo check -p gl --- crates/gitlawb-node/src/api/agents.rs | 35 ++++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/crates/gitlawb-node/src/api/agents.rs b/crates/gitlawb-node/src/api/agents.rs index 1c3eef9..dea088a 100644 --- a/crates/gitlawb-node/src/api/agents.rs +++ b/crates/gitlawb-node/src/api/agents.rs @@ -16,6 +16,29 @@ fn normalize_agent_did(did: &str) -> String { } } +fn agent_key_segment(did: &str) -> &str { + did.split(':').next_back().unwrap_or(did) +} + +async fn resolve_agent_did(state: &AppState, did: &str) -> Result { + let normalized_did = normalize_agent_did(did); + if state.db.get_agent(&normalized_did).await?.is_some() { + return Ok(normalized_did); + } + + let requested_key = agent_key_segment(&normalized_did); + let matching_agent = state + .db + .list_agents(None) + .await? + .into_iter() + .find(|agent| agent_key_segment(&agent.did).starts_with(requested_key)); + + Ok(matching_agent + .map(|agent| agent.did) + .unwrap_or(normalized_did)) +} + #[derive(Debug, Serialize)] pub struct TrustResponse { pub did: String, @@ -74,7 +97,7 @@ pub async fn show_agent( State(state): State, Path(did): Path, ) -> Result<(StatusCode, Json)> { - let normalized_did = normalize_agent_did(&did); + let normalized_did = resolve_agent_did(&state, &did).await?; let agent = state .db .get_agent(&normalized_did) @@ -97,7 +120,7 @@ pub async fn get_trust( State(state): State, Path(did): Path, ) -> Result> { - let normalized_did = normalize_agent_did(&did); + let normalized_did = resolve_agent_did(&state, &did).await?; let trust_score = state.db.get_trust_score(&normalized_did).await?; let push_count = state.db.get_push_count(&normalized_did).await?; let level = trust_level(trust_score); @@ -112,7 +135,7 @@ pub async fn get_trust( #[cfg(test)] mod tests { - use super::normalize_agent_did; + use super::{agent_key_segment, normalize_agent_did}; #[test] fn normalize_agent_did_preserves_full_did() { @@ -125,4 +148,10 @@ mod tests { fn normalize_agent_did_expands_bare_key() { assert_eq!(normalize_agent_did("z6MkExample"), "did:key:z6MkExample"); } + + #[test] + fn agent_key_segment_extracts_did_key_material() { + assert_eq!(agent_key_segment("did:key:z6MkExample"), "z6MkExample"); + assert_eq!(agent_key_segment("z6MkExample"), "z6MkExample"); + } }