Skip to main content

clawdentity_core/registry/
agent.rs

1use std::fs;
2use std::path::{Path, PathBuf};
3use std::time::{Duration, SystemTime, UNIX_EPOCH};
4
5use base64::Engine;
6use base64::engine::general_purpose::URL_SAFE_NO_PAD;
7use ed25519_dalek::{Signer, SigningKey, VerifyingKey};
8use getrandom::fill as getrandom_fill;
9use reqwest::header::{AUTHORIZATION, CONTENT_TYPE};
10use serde::{Deserialize, Serialize};
11
12use crate::config::{ConfigPathOptions, get_config_dir, resolve_config};
13use crate::constants::{AGENTS_DIR, AIT_FILE_NAME, SECRET_KEY_FILE_NAME};
14use crate::error::{CoreError, Result};
15use crate::http::blocking_client;
16use crate::identity::decode_secret_key;
17use crate::signing::{SignHttpRequestInput, sign_http_request};
18
19const FILE_MODE: u32 = 0o600;
20const IDENTITY_FILE: &str = "identity.json";
21const PUBLIC_KEY_FILE: &str = "public.key";
22const REGISTRY_AUTH_FILE: &str = "registry-auth.json";
23
24const AGENT_REGISTRATION_CHALLENGE_PATH: &str = "/v1/agents/challenge";
25const AGENT_REGISTRATION_PATH: &str = "/v1/agents";
26const AGENT_AUTH_REFRESH_PATH: &str = "/v1/agents/auth/refresh";
27const AGENT_REGISTRATION_PROOF_VERSION: &str = "clawdentity.register.v1";
28
29#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
30#[serde(rename_all = "camelCase")]
31pub struct AgentIdentityRecord {
32    pub did: String,
33    pub name: String,
34    pub framework: String,
35    pub expires_at: String,
36    pub registry_url: String,
37}
38
39#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
40#[serde(rename_all = "camelCase")]
41pub struct AgentAuthRecord {
42    pub token_type: String,
43    pub access_token: String,
44    pub access_expires_at: String,
45    pub refresh_token: String,
46    pub refresh_expires_at: String,
47}
48
49#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
50#[serde(rename_all = "camelCase")]
51pub struct AgentCreateResult {
52    pub name: String,
53    pub did: String,
54    pub expires_at: String,
55    pub framework: String,
56    pub registry_url: String,
57}
58
59#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
60#[serde(rename_all = "camelCase")]
61pub struct AgentInspectResult {
62    pub did: String,
63    pub owner_did: String,
64    pub expires_at: String,
65    pub key_id: String,
66    pub public_key: String,
67    pub framework: String,
68}
69
70#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
71#[serde(rename_all = "camelCase")]
72pub struct AgentAuthRefreshResult {
73    pub name: String,
74    pub status: String,
75    pub message: String,
76}
77
78#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
79#[serde(rename_all = "camelCase")]
80pub struct AgentAuthRevokeResult {
81    pub name: String,
82    pub status: String,
83    pub message: String,
84}
85
86#[derive(Debug, Clone, PartialEq, Eq)]
87pub struct CreateAgentInput {
88    pub name: String,
89    pub framework: Option<String>,
90    pub ttl_days: Option<u32>,
91}
92
93#[derive(Debug, Deserialize)]
94#[serde(rename_all = "camelCase")]
95struct AgentRegistrationChallengeResponse {
96    challenge_id: String,
97    nonce: String,
98    owner_did: String,
99}
100
101#[derive(Debug, Deserialize)]
102#[serde(rename_all = "camelCase")]
103struct AgentRegistrationResponse {
104    agent: RegisteredAgentPayload,
105    ait: String,
106    agent_auth: AgentAuthRecord,
107}
108
109#[derive(Debug, Deserialize)]
110#[serde(rename_all = "camelCase")]
111struct RegisteredAgentPayload {
112    did: String,
113    name: String,
114    framework: String,
115    expires_at: String,
116}
117
118#[derive(Debug, Deserialize)]
119#[serde(rename_all = "camelCase")]
120struct ErrorEnvelope {
121    error: Option<RegistryError>,
122}
123
124#[derive(Debug, Deserialize)]
125#[serde(rename_all = "camelCase")]
126struct RegistryError {
127    message: Option<String>,
128}
129
130fn set_secure_permissions(path: &Path) -> Result<()> {
131    #[cfg(unix)]
132    {
133        use std::os::unix::fs::PermissionsExt;
134        let perms = fs::Permissions::from_mode(FILE_MODE);
135        fs::set_permissions(path, perms).map_err(|source| CoreError::Io {
136            path: path.to_path_buf(),
137            source,
138        })?;
139    }
140    Ok(())
141}
142
143fn write_secure_text(path: &Path, contents: &str) -> Result<()> {
144    if let Some(parent) = path.parent() {
145        fs::create_dir_all(parent).map_err(|source| CoreError::Io {
146            path: parent.to_path_buf(),
147            source,
148        })?;
149    }
150    fs::write(path, contents).map_err(|source| CoreError::Io {
151        path: path.to_path_buf(),
152        source,
153    })?;
154    set_secure_permissions(path)?;
155    Ok(())
156}
157
158fn write_secure_json<T: Serialize>(path: &Path, value: &T) -> Result<()> {
159    let body = serde_json::to_string_pretty(value)?;
160    write_secure_text(path, &format!("{body}\n"))
161}
162
163fn read_json<T: for<'de> Deserialize<'de>>(path: &Path) -> Result<T> {
164    let raw = fs::read_to_string(path).map_err(|source| CoreError::Io {
165        path: path.to_path_buf(),
166        source,
167    })?;
168    serde_json::from_str::<T>(&raw).map_err(|source| CoreError::JsonParse {
169        path: path.to_path_buf(),
170        source,
171    })
172}
173
174fn parse_agent_name(name: &str) -> Result<String> {
175    let candidate = name.trim();
176    if candidate.is_empty() {
177        return Err(CoreError::InvalidInput(
178            "agent name is required".to_string(),
179        ));
180    }
181    if candidate == "." || candidate == ".." {
182        return Err(CoreError::InvalidInput(
183            "agent name must not be . or ..".to_string(),
184        ));
185    }
186    if candidate.len() > 64 {
187        return Err(CoreError::InvalidInput(
188            "agent name must be <= 64 characters".to_string(),
189        ));
190    }
191    let valid = candidate
192        .chars()
193        .all(|ch| ch.is_ascii_alphanumeric() || ch == '-' || ch == '_' || ch == '.');
194    if !valid {
195        return Err(CoreError::InvalidInput(
196            "agent name contains invalid characters".to_string(),
197        ));
198    }
199    Ok(candidate.to_string())
200}
201
202fn parse_optional_framework(value: Option<String>) -> Result<Option<String>> {
203    let Some(raw) = value else {
204        return Ok(None);
205    };
206    let normalized = raw.trim();
207    if normalized.is_empty() {
208        return Err(CoreError::InvalidInput(
209            "framework cannot be empty when provided".to_string(),
210        ));
211    }
212    Ok(Some(normalized.to_string()))
213}
214
215fn parse_optional_ttl_days(value: Option<u32>) -> Result<Option<u32>> {
216    match value {
217        Some(0) => Err(CoreError::InvalidInput(
218            "ttlDays must be a positive integer".to_string(),
219        )),
220        Some(days) => Ok(Some(days)),
221        None => Ok(None),
222    }
223}
224
225fn agents_dir(options: &ConfigPathOptions) -> Result<PathBuf> {
226    Ok(get_config_dir(options)?.join(AGENTS_DIR))
227}
228
229fn agent_dir(options: &ConfigPathOptions, name: &str) -> Result<PathBuf> {
230    Ok(agents_dir(options)?.join(name))
231}
232
233fn now_unix_seconds() -> Result<u64> {
234    Ok(SystemTime::now()
235        .duration_since(UNIX_EPOCH)
236        .map_err(|error| CoreError::InvalidInput(error.to_string()))?
237        .as_secs())
238}
239
240fn unix_seconds_to_iso(seconds: u64) -> Result<String> {
241    let dt = UNIX_EPOCH
242        .checked_add(Duration::from_secs(seconds))
243        .ok_or_else(|| CoreError::InvalidInput("invalid timestamp".to_string()))?;
244    let datetime: chrono::DateTime<chrono::Utc> = dt.into();
245    Ok(datetime.to_rfc3339())
246}
247
248fn decode_jwt_payload(token: &str) -> Result<serde_json::Value> {
249    let parts: Vec<&str> = token.split('.').collect();
250    if parts.len() < 2 {
251        return Err(CoreError::InvalidInput("invalid AIT token".to_string()));
252    }
253    let payload = URL_SAFE_NO_PAD
254        .decode(parts[1])
255        .map_err(|error| CoreError::Base64Decode(error.to_string()))?;
256    serde_json::from_slice(&payload).map_err(|error| CoreError::InvalidInput(error.to_string()))
257}
258
259fn decode_jwt_header(token: &str) -> Result<serde_json::Value> {
260    let parts: Vec<&str> = token.split('.').collect();
261    if parts.is_empty() {
262        return Err(CoreError::InvalidInput("invalid AIT token".to_string()));
263    }
264    let header = URL_SAFE_NO_PAD
265        .decode(parts[0])
266        .map_err(|error| CoreError::Base64Decode(error.to_string()))?;
267    serde_json::from_slice(&header).map_err(|error| CoreError::InvalidInput(error.to_string()))
268}
269
270fn join_url(base: &str, path: &str) -> Result<String> {
271    let base_url = url::Url::parse(base).map_err(|_| CoreError::InvalidUrl {
272        context: "registryUrl",
273        value: base.to_string(),
274    })?;
275    let joined = base_url.join(path).map_err(|_| CoreError::InvalidUrl {
276        context: "registryUrl",
277        value: base.to_string(),
278    })?;
279    Ok(joined.to_string())
280}
281
282fn parse_error_message(response_body: &str) -> String {
283    match serde_json::from_str::<ErrorEnvelope>(response_body) {
284        Ok(envelope) => envelope
285            .error
286            .and_then(|error| error.message)
287            .unwrap_or_else(|| response_body.to_string()),
288        Err(_) => response_body.to_string(),
289    }
290}
291
292fn canonicalize_agent_registration_proof(input: &CanonicalProofInput<'_>) -> String {
293    [
294        AGENT_REGISTRATION_PROOF_VERSION.to_string(),
295        format!("challengeId:{}", input.challenge_id),
296        format!("nonce:{}", input.nonce),
297        format!("ownerDid:{}", input.owner_did),
298        format!("publicKey:{}", input.public_key),
299        format!("name:{}", input.name),
300        format!("framework:{}", input.framework.unwrap_or("")),
301        format!(
302            "ttlDays:{}",
303            input
304                .ttl_days
305                .map(|value| value.to_string())
306                .unwrap_or_default()
307        ),
308    ]
309    .join("\n")
310}
311
312struct CanonicalProofInput<'a> {
313    challenge_id: &'a str,
314    nonce: &'a str,
315    owner_did: &'a str,
316    public_key: &'a str,
317    name: &'a str,
318    framework: Option<&'a str>,
319    ttl_days: Option<u32>,
320}
321
322struct AgentRegistrationRequest<'a> {
323    name: &'a str,
324    public_key: &'a str,
325    challenge_id: &'a str,
326    challenge_signature: &'a str,
327    framework: Option<&'a str>,
328    ttl_days: Option<u32>,
329}
330
331fn request_registration_challenge(
332    client: &reqwest::blocking::Client,
333    registry_url: &str,
334    api_key: &str,
335    public_key: &str,
336) -> Result<AgentRegistrationChallengeResponse> {
337    let url = join_url(registry_url, AGENT_REGISTRATION_CHALLENGE_PATH)?;
338    let response = client
339        .post(url)
340        .header(AUTHORIZATION, format!("Bearer {api_key}"))
341        .header(CONTENT_TYPE, "application/json")
342        .json(&serde_json::json!({
343            "publicKey": public_key,
344        }))
345        .send()
346        .map_err(|error| CoreError::Http(error.to_string()))?;
347
348    if !response.status().is_success() {
349        let status = response.status().as_u16();
350        let response_body = response.text().unwrap_or_default();
351        let message = parse_error_message(&response_body);
352        return Err(CoreError::HttpStatus { status, message });
353    }
354
355    response
356        .json::<AgentRegistrationChallengeResponse>()
357        .map_err(|error| CoreError::Http(error.to_string()))
358}
359
360fn request_agent_registration(
361    client: &reqwest::blocking::Client,
362    registry_url: &str,
363    api_key: &str,
364    input: AgentRegistrationRequest<'_>,
365) -> Result<AgentRegistrationResponse> {
366    let mut request_body = serde_json::json!({
367        "name": input.name,
368        "publicKey": input.public_key,
369        "challengeId": input.challenge_id,
370        "challengeSignature": input.challenge_signature,
371    });
372    if let Some(value) = input.framework {
373        request_body["framework"] = serde_json::Value::String(value.to_string());
374    }
375    if let Some(value) = input.ttl_days {
376        request_body["ttlDays"] = serde_json::Value::Number(value.into());
377    }
378
379    let url = join_url(registry_url, AGENT_REGISTRATION_PATH)?;
380    let response = client
381        .post(url)
382        .header(AUTHORIZATION, format!("Bearer {api_key}"))
383        .header(CONTENT_TYPE, "application/json")
384        .json(&request_body)
385        .send()
386        .map_err(|error| CoreError::Http(error.to_string()))?;
387
388    if !response.status().is_success() {
389        let status = response.status().as_u16();
390        let response_body = response.text().unwrap_or_default();
391        let message = parse_error_message(&response_body);
392        return Err(CoreError::HttpStatus { status, message });
393    }
394
395    response
396        .json::<AgentRegistrationResponse>()
397        .map_err(|error| CoreError::Http(error.to_string()))
398}
399
400fn random_nonce_base64url(size: usize) -> Result<String> {
401    let mut nonce = vec![0_u8; size];
402    getrandom_fill(&mut nonce).map_err(|error| CoreError::InvalidInput(error.to_string()))?;
403    Ok(URL_SAFE_NO_PAD.encode(nonce))
404}
405
406fn path_with_query(request_url: &str) -> Result<String> {
407    let parsed = url::Url::parse(request_url).map_err(|_| CoreError::InvalidUrl {
408        context: "registryUrl",
409        value: request_url.to_string(),
410    })?;
411    Ok(match parsed.query() {
412        Some(query) => format!("{}?{query}", parsed.path()),
413        None => parsed.path().to_string(),
414    })
415}
416
417fn parse_agent_auth_response(payload: serde_json::Value) -> Result<AgentAuthRecord> {
418    let source = payload.get("agentAuth").cloned().unwrap_or(payload);
419    let parsed = serde_json::from_value::<AgentAuthRecord>(source)
420        .map_err(|error| CoreError::InvalidInput(error.to_string()))?;
421    if parsed.token_type != "Bearer" {
422        return Err(CoreError::InvalidInput(
423            "invalid tokenType in agentAuth response".to_string(),
424        ));
425    }
426    Ok(parsed)
427}
428
429/// TODO(clawdentity): document `create_agent`.
430#[allow(clippy::too_many_lines)]
431pub fn create_agent(
432    options: &ConfigPathOptions,
433    input: CreateAgentInput,
434) -> Result<AgentCreateResult> {
435    let config = resolve_config(options)?;
436    let api_key = config.api_key.ok_or_else(|| {
437        CoreError::InvalidInput(
438            "API key is not configured. Run `clawdentity config set apiKey <token>` first."
439                .to_string(),
440        )
441    })?;
442
443    let name = parse_agent_name(&input.name)?;
444    let framework = parse_optional_framework(input.framework)?;
445    let ttl_days = parse_optional_ttl_days(input.ttl_days)?;
446
447    let state_options = options.with_registry_hint(config.registry_url.clone());
448    let agent_directory = agent_dir(&state_options, &name)?;
449    if agent_directory.exists() {
450        return Err(CoreError::IdentityAlreadyExists(agent_directory));
451    }
452    fs::create_dir_all(&agent_directory).map_err(|source| CoreError::Io {
453        path: agent_directory.clone(),
454        source,
455    })?;
456
457    let mut secret_bytes = [0_u8; 32];
458    getrandom_fill(&mut secret_bytes)
459        .map_err(|error| CoreError::InvalidInput(error.to_string()))?;
460    let signing_key = SigningKey::from_bytes(&secret_bytes);
461    let verifying_key: VerifyingKey = signing_key.verifying_key();
462
463    let public_key = URL_SAFE_NO_PAD.encode(verifying_key.as_bytes());
464    let secret_key = URL_SAFE_NO_PAD.encode(signing_key.to_bytes());
465
466    let client = blocking_client()?;
467    let challenge =
468        request_registration_challenge(&client, &config.registry_url, &api_key, &public_key)?;
469    let canonical_proof = canonicalize_agent_registration_proof(&CanonicalProofInput {
470        challenge_id: &challenge.challenge_id,
471        nonce: &challenge.nonce,
472        owner_did: &challenge.owner_did,
473        public_key: &public_key,
474        name: &name,
475        framework: framework.as_deref(),
476        ttl_days,
477    });
478    let challenge_signature =
479        URL_SAFE_NO_PAD.encode(signing_key.sign(canonical_proof.as_bytes()).to_bytes());
480
481    let registration = request_agent_registration(
482        &client,
483        &config.registry_url,
484        &api_key,
485        AgentRegistrationRequest {
486            name: &name,
487            public_key: &public_key,
488            challenge_id: &challenge.challenge_id,
489            challenge_signature: &challenge_signature,
490            framework: framework.as_deref(),
491            ttl_days,
492        },
493    )?;
494
495    let identity = AgentIdentityRecord {
496        did: registration.agent.did.clone(),
497        name: registration.agent.name.clone(),
498        framework: registration.agent.framework.clone(),
499        expires_at: registration.agent.expires_at.clone(),
500        registry_url: config.registry_url.clone(),
501    };
502
503    write_secure_json(&agent_directory.join(IDENTITY_FILE), &identity)?;
504    write_secure_text(
505        &agent_directory.join(AIT_FILE_NAME),
506        registration.ait.trim(),
507    )?;
508    write_secure_text(&agent_directory.join(SECRET_KEY_FILE_NAME), &secret_key)?;
509    write_secure_text(&agent_directory.join(PUBLIC_KEY_FILE), &public_key)?;
510    write_secure_json(
511        &agent_directory.join(REGISTRY_AUTH_FILE),
512        &registration.agent_auth,
513    )?;
514
515    Ok(AgentCreateResult {
516        name: registration.agent.name,
517        did: registration.agent.did,
518        expires_at: registration.agent.expires_at,
519        framework: registration.agent.framework,
520        registry_url: config.registry_url,
521    })
522}
523
524/// TODO(clawdentity): document `inspect_agent`.
525#[allow(clippy::too_many_lines)]
526pub fn inspect_agent(options: &ConfigPathOptions, name: &str) -> Result<AgentInspectResult> {
527    let name = parse_agent_name(name)?;
528    let agent_directory = agent_dir(options, &name)?;
529    let ait_path = agent_directory.join(AIT_FILE_NAME);
530    let raw = fs::read_to_string(&ait_path).map_err(|source| CoreError::Io {
531        path: ait_path.clone(),
532        source,
533    })?;
534    let token = raw.trim();
535    let header = decode_jwt_header(token)?;
536    let payload = decode_jwt_payload(token)?;
537
538    let did = payload
539        .get("sub")
540        .and_then(|value| value.as_str())
541        .unwrap_or_default()
542        .to_string();
543    let owner_did = payload
544        .get("ownerDid")
545        .and_then(|value| value.as_str())
546        .unwrap_or_default()
547        .to_string();
548    let framework = payload
549        .get("framework")
550        .and_then(|value| value.as_str())
551        .unwrap_or("openclaw")
552        .to_string();
553    let public_key = payload
554        .get("cnf")
555        .and_then(|value| value.get("jwk"))
556        .and_then(|value| value.get("x"))
557        .and_then(|value| value.as_str())
558        .unwrap_or_default()
559        .to_string();
560    let key_id = header
561        .get("kid")
562        .and_then(|value| value.as_str())
563        .unwrap_or_default()
564        .to_string();
565    let exp = payload
566        .get("exp")
567        .and_then(|value| value.as_u64())
568        .unwrap_or_default();
569    let expires_at = unix_seconds_to_iso(exp)?;
570
571    if did.is_empty() || owner_did.is_empty() || key_id.is_empty() || public_key.is_empty() {
572        return Err(CoreError::InvalidInput(
573            "agent AIT payload is invalid".to_string(),
574        ));
575    }
576
577    Ok(AgentInspectResult {
578        did,
579        owner_did,
580        expires_at,
581        key_id,
582        public_key,
583        framework,
584    })
585}
586
587/// TODO(clawdentity): document `refresh_agent_auth`.
588#[allow(clippy::too_many_lines)]
589pub fn refresh_agent_auth(
590    options: &ConfigPathOptions,
591    name: &str,
592) -> Result<AgentAuthRefreshResult> {
593    let name = parse_agent_name(name)?;
594    let agent_directory = agent_dir(options, &name)?;
595
596    let auth_path = agent_directory.join(REGISTRY_AUTH_FILE);
597    let current_auth: AgentAuthRecord = read_json(&auth_path)?;
598
599    let identity_path = agent_directory.join(IDENTITY_FILE);
600    let identity: AgentIdentityRecord = read_json(&identity_path)?;
601    if identity.registry_url.trim().is_empty() {
602        return Err(CoreError::InvalidInput(
603            "agent identity is missing registryUrl".to_string(),
604        ));
605    }
606
607    let ait_path = agent_directory.join(AIT_FILE_NAME);
608    let ait_raw = fs::read_to_string(&ait_path).map_err(|source| CoreError::Io {
609        path: ait_path.clone(),
610        source,
611    })?;
612    let ait = ait_raw.trim();
613    if ait.is_empty() {
614        return Err(CoreError::InvalidInput(
615            "agent AIT token is empty".to_string(),
616        ));
617    }
618
619    let secret_path = agent_directory.join(SECRET_KEY_FILE_NAME);
620    let secret_raw = fs::read_to_string(&secret_path).map_err(|source| CoreError::Io {
621        path: secret_path.clone(),
622        source,
623    })?;
624    let signing_key = decode_secret_key(secret_raw.trim())?;
625
626    let request_body = serde_json::json!({
627        "refreshToken": current_auth.refresh_token,
628    });
629    let request_body_bytes = serde_json::to_vec(&request_body)?;
630    let refresh_url = join_url(&identity.registry_url, AGENT_AUTH_REFRESH_PATH)?;
631    let path_with_query = path_with_query(&refresh_url)?;
632    let timestamp = now_unix_seconds()?.to_string();
633    let nonce = random_nonce_base64url(16)?;
634    let signed = sign_http_request(&SignHttpRequestInput {
635        method: "POST",
636        path_with_query: &path_with_query,
637        timestamp: &timestamp,
638        nonce: &nonce,
639        body: &request_body_bytes,
640        secret_key: &signing_key,
641    })?;
642
643    let mut request = blocking_client()?
644        .post(refresh_url)
645        .header(AUTHORIZATION, format!("Claw {ait}"))
646        .header(CONTENT_TYPE, "application/json");
647    for (header_name, value) in signed.headers {
648        request = request.header(&header_name, value);
649    }
650
651    let response = request
652        .body(request_body_bytes)
653        .send()
654        .map_err(|error| CoreError::Http(error.to_string()))?;
655
656    if !response.status().is_success() {
657        let status = response.status().as_u16();
658        let response_body = response.text().unwrap_or_default();
659        let message = parse_error_message(&response_body);
660        return Err(CoreError::HttpStatus { status, message });
661    }
662
663    let payload = response
664        .json::<serde_json::Value>()
665        .map_err(|error| CoreError::Http(error.to_string()))?;
666    let refreshed = parse_agent_auth_response(payload)?;
667    write_secure_json(&auth_path, &refreshed)?;
668
669    Ok(AgentAuthRefreshResult {
670        name,
671        status: "refreshed".to_string(),
672        message: "agent auth bundle updated".to_string(),
673    })
674}
675
676/// TODO(clawdentity): document `revoke_agent_auth`.
677pub fn revoke_agent_auth(options: &ConfigPathOptions, name: &str) -> Result<AgentAuthRevokeResult> {
678    let name = parse_agent_name(name)?;
679    let agent_directory = agent_dir(options, &name)?;
680    let auth_path = agent_directory.join(REGISTRY_AUTH_FILE);
681    let _: AgentAuthRecord = read_json(&auth_path)?;
682
683    Ok(AgentAuthRevokeResult {
684        name,
685        status: "not_supported".to_string(),
686        message: "not yet supported by registry".to_string(),
687    })
688}
689
690#[cfg(test)]
691#[path = "agent_tests.rs"]
692mod tests;