Skip to content

Commit b88994d

Browse files
committed
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
1 parent f312955 commit b88994d

2 files changed

Lines changed: 82 additions & 18 deletions

File tree

crates/gitlawb-node/src/api/agents.rs

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,14 @@ use serde::{Deserialize, Serialize};
88
use crate::error::{AppError, Result};
99
use crate::state::AppState;
1010

11+
fn normalize_agent_did(did: &str) -> String {
12+
if did.starts_with("did:") {
13+
did.to_string()
14+
} else {
15+
format!("did:key:{did}")
16+
}
17+
}
18+
1119
#[derive(Debug, Serialize)]
1220
pub struct TrustResponse {
1321
pub did: String,
@@ -66,11 +74,12 @@ pub async fn show_agent(
6674
State(state): State<AppState>,
6775
Path(did): Path<String>,
6876
) -> Result<(StatusCode, Json<AgentResponse>)> {
77+
let normalized_did = normalize_agent_did(&did);
6978
let agent = state
7079
.db
71-
.get_agent(&did)
80+
.get_agent(&normalized_did)
7281
.await?
73-
.ok_or_else(|| AppError::NotFound(format!("agent {did} not found")))?;
82+
.ok_or_else(|| AppError::NotFound(format!("agent {normalized_did} not found")))?;
7483
Ok((
7584
StatusCode::OK,
7685
Json(AgentResponse {
@@ -88,14 +97,32 @@ pub async fn get_trust(
8897
State(state): State<AppState>,
8998
Path(did): Path<String>,
9099
) -> Result<Json<TrustResponse>> {
91-
let trust_score = state.db.get_trust_score(&did).await?;
92-
let push_count = state.db.get_push_count(&did).await?;
100+
let normalized_did = normalize_agent_did(&did);
101+
let trust_score = state.db.get_trust_score(&normalized_did).await?;
102+
let push_count = state.db.get_push_count(&normalized_did).await?;
93103
let level = trust_level(trust_score);
94104

95105
Ok(Json(TrustResponse {
96-
did,
106+
did: normalized_did,
97107
trust_score,
98108
push_count,
99109
level,
100110
}))
101111
}
112+
113+
#[cfg(test)]
114+
mod tests {
115+
use super::normalize_agent_did;
116+
117+
#[test]
118+
fn normalize_agent_did_preserves_full_did() {
119+
let did = "did:key:z6MkExample";
120+
121+
assert_eq!(normalize_agent_did(did), did);
122+
}
123+
124+
#[test]
125+
fn normalize_agent_did_expands_bare_key() {
126+
assert_eq!(normalize_agent_did("z6MkExample"), "did:key:z6MkExample");
127+
}
128+
}

crates/gl/src/cert.rs

Lines changed: 50 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use clap::{Args, Subcommand};
77
use serde_json::Value;
88

99
use crate::http::NodeClient;
10+
use crate::identity::load_keypair_from_dir;
1011

1112
#[derive(Args)]
1213
pub struct CertArgs {
@@ -41,20 +42,25 @@ pub async fn run(args: CertArgs) -> Result<()> {
4142
}
4243
}
4344

44-
/// Resolve "repo" into (owner, name) — if no slash, use the node's own DID short form.
45+
/// Resolve "repo" into (owner, name) using the caller's DID when no slash is given.
4546
async fn resolve_repo(repo: &str, node: &str) -> Result<(String, String)> {
4647
if let Some((owner, name)) = repo.split_once('/') {
4748
Ok((owner.to_string(), name.to_string()))
4849
} else {
49-
let client = NodeClient::new(node, None);
50-
let info: Value = client
51-
.get("/")
52-
.await?
53-
.json()
54-
.await
55-
.context("failed to fetch node info")?;
56-
let did = info["did"].as_str().context("node info missing 'did'")?;
57-
let short = did.split(':').next_back().unwrap_or(did).to_string();
50+
let short = if let Ok(kp) = load_keypair_from_dir(None) {
51+
let did = kp.did().to_string();
52+
did.split(':').next_back().unwrap_or(&did).to_string()
53+
} else {
54+
let client = NodeClient::new(node, None);
55+
let info: Value = client
56+
.get("/")
57+
.await?
58+
.json()
59+
.await
60+
.context("failed to fetch node info")?;
61+
let did = info["did"].as_str().context("node info missing 'did'")?;
62+
did.split(':').next_back().unwrap_or(did).to_string()
63+
};
5864
Ok((short, repo.to_string()))
5965
}
6066
}
@@ -94,15 +100,16 @@ async fn cmd_show(repo: String, id: String, node: String) -> Result<()> {
94100
let (owner, name) = resolve_repo(&repo, &node).await?;
95101

96102
let client = NodeClient::new(&node, None);
103+
let id = resolve_cert_id(&client, &owner, &name, &id).await?;
97104

98105
// Fetch the certificate
99106
let path = format!("/api/v1/repos/{owner}/{name}/certs/{id}");
100-
let cert: Value = client
107+
let resp = client
101108
.get(&path)
102109
.await?
103-
.json()
104-
.await
110+
.error_for_status()
105111
.context("certificate not found")?;
112+
let cert: Value = resp.json().await.context("certificate not found")?;
106113

107114
let cert_id = cert["id"].as_str().unwrap_or("?");
108115
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<()> {
155162

156163
Ok(())
157164
}
165+
166+
async fn resolve_cert_id(client: &NodeClient, owner: &str, name: &str, id: &str) -> Result<String> {
167+
if id.len() >= 36 {
168+
return Ok(id.to_string());
169+
}
170+
171+
let path = format!("/api/v1/repos/{owner}/{name}/certs");
172+
let resp: Value = client
173+
.get(&path)
174+
.await?
175+
.error_for_status()
176+
.context("failed to list certificates")?
177+
.json()
178+
.await
179+
.context("failed to list certificates")?;
180+
181+
let certs = resp["certificates"].as_array().cloned().unwrap_or_default();
182+
let matches: Vec<String> = certs
183+
.iter()
184+
.filter_map(|cert| cert["id"].as_str())
185+
.filter(|cert_id| cert_id.starts_with(id))
186+
.map(ToString::to_string)
187+
.collect();
188+
189+
match matches.as_slice() {
190+
[full_id] => Ok(full_id.to_string()),
191+
[] => Ok(id.to_string()),
192+
_ => anyhow::bail!("certificate prefix {id} matches multiple certificates"),
193+
}
194+
}

0 commit comments

Comments
 (0)