Skip to content

Commit f3df4a4

Browse files
authored
Resolve bare DID aliases for trust and cert lookups (#9)
* 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 * 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
1 parent 034cb3c commit f3df4a4

2 files changed

Lines changed: 111 additions & 18 deletions

File tree

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

Lines changed: 61 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,37 @@ 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+
19+
fn agent_key_segment(did: &str) -> &str {
20+
did.split(':').next_back().unwrap_or(did)
21+
}
22+
23+
async fn resolve_agent_did(state: &AppState, did: &str) -> Result<String> {
24+
let normalized_did = normalize_agent_did(did);
25+
if state.db.get_agent(&normalized_did).await?.is_some() {
26+
return Ok(normalized_did);
27+
}
28+
29+
let requested_key = agent_key_segment(&normalized_did);
30+
let matching_agent = state
31+
.db
32+
.list_agents(None)
33+
.await?
34+
.into_iter()
35+
.find(|agent| agent_key_segment(&agent.did).starts_with(requested_key));
36+
37+
Ok(matching_agent
38+
.map(|agent| agent.did)
39+
.unwrap_or(normalized_did))
40+
}
41+
1142
#[derive(Debug, Serialize)]
1243
pub struct TrustResponse {
1344
pub did: String,
@@ -66,11 +97,12 @@ pub async fn show_agent(
6697
State(state): State<AppState>,
6798
Path(did): Path<String>,
6899
) -> Result<(StatusCode, Json<AgentResponse>)> {
100+
let normalized_did = resolve_agent_did(&state, &did).await?;
69101
let agent = state
70102
.db
71-
.get_agent(&did)
103+
.get_agent(&normalized_did)
72104
.await?
73-
.ok_or_else(|| AppError::NotFound(format!("agent {did} not found")))?;
105+
.ok_or_else(|| AppError::NotFound(format!("agent {normalized_did} not found")))?;
74106
Ok((
75107
StatusCode::OK,
76108
Json(AgentResponse {
@@ -88,14 +120,38 @@ pub async fn get_trust(
88120
State(state): State<AppState>,
89121
Path(did): Path<String>,
90122
) -> 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?;
123+
let normalized_did = resolve_agent_did(&state, &did).await?;
124+
let trust_score = state.db.get_trust_score(&normalized_did).await?;
125+
let push_count = state.db.get_push_count(&normalized_did).await?;
93126
let level = trust_level(trust_score);
94127

95128
Ok(Json(TrustResponse {
96-
did,
129+
did: normalized_did,
97130
trust_score,
98131
push_count,
99132
level,
100133
}))
101134
}
135+
136+
#[cfg(test)]
137+
mod tests {
138+
use super::{agent_key_segment, normalize_agent_did};
139+
140+
#[test]
141+
fn normalize_agent_did_preserves_full_did() {
142+
let did = "did:key:z6MkExample";
143+
144+
assert_eq!(normalize_agent_did(did), did);
145+
}
146+
147+
#[test]
148+
fn normalize_agent_did_expands_bare_key() {
149+
assert_eq!(normalize_agent_did("z6MkExample"), "did:key:z6MkExample");
150+
}
151+
152+
#[test]
153+
fn agent_key_segment_extracts_did_key_material() {
154+
assert_eq!(agent_key_segment("did:key:z6MkExample"), "z6MkExample");
155+
assert_eq!(agent_key_segment("z6MkExample"), "z6MkExample");
156+
}
157+
}

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)