Skip to main content

loa_core/
identity.rs

1//! Agent Identity Management
2//!
3//! Handles Ed25519 keypairs for signing and X25519 keypairs for encryption.
4//!
5//! # Key Usage
6//! - **Ed25519**: Currently used for heartbeat authentication (signing messages)
7//! - **X25519**: Reserved for future encrypted agent-to-agent communication
8//!   (encrypted metrics, encrypted commands, etc.). The API stores both public
9//!   keys but only validates Ed25519 signatures currently.
10
11use base64::{engine::general_purpose::STANDARD as BASE64, Engine};
12use crate::{Error, Result};
13use libp2p_identity::{ed25519, Keypair, PeerId};
14use once_cell::sync::OnceCell;
15use rand::rngs::OsRng;
16use serde::{Deserialize, Serialize};
17use std::{fs, path::Path, path::PathBuf, sync::Arc};
18use x25519_dalek::{PublicKey as X25519PublicKey, StaticSecret as X25519SecretKey};
19
20/// Filename for consolidated agent data (new format)
21const AGENT_TOML_FILE: &str = "agent.toml";
22/// Filename for legacy key storage (old format)
23const LEGACY_KEY_FILE: &str = "agent_id.key";
24/// Filename for legacy claim token (old format)
25const LEGACY_CLAIM_TOKEN_FILE: &str = "claim_token";
26/// Filename for legacy agent name (old format)
27const LEGACY_AGENT_NAME_FILE: &str = "agent_name";
28
29/// Lightweight agent information that can be read without starting the runtime.
30///
31/// Use this for CLI info commands, status checks, or any scenario where you need
32/// agent metadata without spawning the full actor system.
33#[derive(Debug, Clone)]
34pub struct AgentInfo {
35    /// Unique agent identifier derived from Ed25519 public key
36    pub peer_id: PeerId,
37    /// Human-readable 3-word name (e.g., "conscious-jade-mongoose")
38    /// None if agent hasn't registered with the server yet
39    pub name: Option<String>,
40    /// Path to agent storage directory
41    pub storage_path: PathBuf,
42    /// Dashboard URL for this agent
43    pub dashboard_url: String,
44    /// Ed25519 public key in hex format
45    pub ed25519_public_key_hex: String,
46    /// X25519 public key in hex format (for future encrypted communication)
47    pub x25519_public_key_hex: String,
48    /// Workspace claim token if agent is claimed
49    pub claim_token: Option<String>,
50}
51
52impl AgentInfo {
53    /// Read agent info from storage without starting the runtime.
54    ///
55    /// This is a lightweight operation that only reads files from disk.
56    /// No actors, databases, or network connections are started.
57    ///
58    /// Tries TOML format first (`agent.toml`), then falls back to legacy
59    /// separate files (`agent_id.key`, `claim_token`, `agent_name`).
60    ///
61    /// # Arguments
62    ///
63    /// * `storage_path` - Path to the agent's storage directory
64    /// * `api_url` - Optional API URL override for computing the dashboard URL.
65    ///   If None, uses the default from constants::api_url().
66    ///
67    /// # Returns
68    ///
69    /// Returns `AgentInfo` if the identity file exists, or an error if the
70    /// agent has not been initialized yet.
71    ///
72    /// # Example
73    ///
74    /// ```no_run
75    /// use elo::AgentInfo;
76    /// use std::path::Path;
77    ///
78    /// let info = AgentInfo::read(Path::new("/var/lib/loa"), None)?;
79    /// println!("Agent: {} ({})", info.name, info.peer_id);
80    /// println!("Dashboard: {}", info.dashboard_url);
81    /// # Ok::<(), elo::Error>(())
82    /// ```
83    pub fn read(storage_path: &Path, api_url: Option<&str>) -> Result<Self> {
84        let toml_path = storage_path.join(AGENT_TOML_FILE);
85        let legacy_path = storage_path.join(LEGACY_KEY_FILE);
86
87        if !toml_path.exists() && !legacy_path.exists() {
88            return Err(Error::Config(format!(
89                "Agent not initialized. No identity found at {} or {}",
90                toml_path.display(),
91                legacy_path.display()
92            )));
93        }
94
95        // Try to read registration data from TOML first
96        let (name, claim_token) = if toml_path.exists() {
97            Self::read_registration_from_toml(&toml_path)?
98        } else {
99            // Fall back to legacy separate files
100            let name = AgentIdentity::read_legacy_file(storage_path, LEGACY_AGENT_NAME_FILE);
101            let claim_token = AgentIdentity::read_legacy_file(storage_path, LEGACY_CLAIM_TOKEN_FILE);
102            (name, claim_token)
103        };
104
105        // Load identity (handles TOML vs legacy automatically)
106        let identity = AgentIdentity::from_storage_or_generate_new(storage_path)?;
107        let peer_id = identity.peer_id().clone();
108
109        // Build status page URL (use name if registered, otherwise peer_id)
110        let api_url = api_url
111            .map(|s| s.to_string())
112            .unwrap_or_else(|| crate::constants::api_url());
113        let peer_id_str = peer_id.to_string();
114        let identifier = name.as_deref().unwrap_or(&peer_id_str);
115        let dashboard_url = if api_url.contains("localhost") {
116            format!("http://status.localhost:3000/{}", identifier)
117        } else if api_url.contains("workers.dev") {
118            // Dev environment
119            format!("https://status.loa-web.pages.dev/{}", identifier)
120        } else {
121            // Production
122            format!("https://status.loa.sh/{}", identifier)
123        };
124
125        Ok(Self {
126            peer_id,
127            name,
128            storage_path: storage_path.to_path_buf(),
129            dashboard_url,
130            ed25519_public_key_hex: identity.ed25519_public_key_hex().to_string(),
131            x25519_public_key_hex: identity.x25519_public_key_hex().to_string(),
132            claim_token,
133        })
134    }
135
136    /// Read registration section from TOML file
137    fn read_registration_from_toml(toml_path: &Path) -> Result<(Option<String>, Option<String>)> {
138        let toml_content = fs::read_to_string(toml_path)
139            .map_err(|e| Error::Io(std::io::Error::new(e.kind(), format!("read agent.toml: {}", e))))?;
140        let agent_toml: AgentToml = toml::from_str(&toml_content)
141            .map_err(|e| Error::Config(format!("parse agent.toml: {}", e)))?;
142
143        let (name, claim_token) = match agent_toml.registration {
144            Some(reg) => (reg.name, reg.claim_token),
145            None => (None, None),
146        };
147
148        Ok((name, claim_token))
149    }
150
151    /// Check if the agent identity exists at the given storage path.
152    ///
153    /// This is a quick check that doesn't load the identity file.
154    /// Checks for both TOML and legacy formats.
155    pub fn exists(storage_path: &Path) -> bool {
156        storage_path.join(AGENT_TOML_FILE).exists()
157            || storage_path.join(LEGACY_KEY_FILE).exists()
158    }
159}
160
161/// Global agent identity singleton
162///
163/// Initialized once during agent startup and accessible from all actors.
164static AGENT_IDENTITY: OnceCell<Arc<AgentIdentity>> = OnceCell::new();
165
166/// Initialize the global agent identity
167///
168/// This should be called once during agent startup, typically in the builder.
169///
170/// # Panics
171///
172/// Panics if called more than once.
173pub fn init_global_identity(identity: Arc<AgentIdentity>) {
174    AGENT_IDENTITY
175        .set(identity)
176        .expect("Global identity already initialized");
177}
178
179/// Get a clone of the global agent identity
180///
181/// Returns an Arc clone for efficient sharing across actors.
182///
183/// # Panics
184///
185/// Panics if called before `init_global_identity()`.
186pub fn get_global_identity() -> Arc<AgentIdentity> {
187    AGENT_IDENTITY
188        .get()
189        .expect("Global identity not initialized - call init_global_identity() first")
190        .clone()
191}
192
193#[derive(Serialize, Deserialize)]
194struct PersistedKeys {
195    ed25519_bytes: Vec<u8>,
196    x25519_bytes: Vec<u8>,
197}
198
199// ============================================================================
200// TOML FORMAT (new consolidated format)
201// ============================================================================
202
203/// TOML structure for consolidated agent persistence
204#[derive(Serialize, Deserialize)]
205struct AgentToml {
206    identity: IdentitySection,
207    #[serde(default)]
208    registration: Option<RegistrationSection>,
209}
210
211#[derive(Serialize, Deserialize)]
212struct IdentitySection {
213    /// Base64-encoded Ed25519 secret key (64 bytes)
214    ed25519_secret: String,
215    /// Base64-encoded X25519 secret key (32 bytes)
216    x25519_secret: String,
217}
218
219#[derive(Serialize, Deserialize, Default, Clone)]
220struct RegistrationSection {
221    /// Human-readable 3-word name (e.g., "conscious-jade-mongoose")
222    #[serde(skip_serializing_if = "Option::is_none")]
223    name: Option<String>,
224    /// Workspace claim token
225    #[serde(skip_serializing_if = "Option::is_none")]
226    claim_token: Option<String>,
227}
228
229pub struct AgentIdentity {
230    ed25519_keypair: Keypair,
231    x25519_secret: X25519SecretKey,
232    /// X25519 public key - reserved for future encrypted communication.
233    /// Currently only sent to API during registration; not used locally yet.
234    #[allow(dead_code)]
235    x25519_public: X25519PublicKey,
236    peer_id: PeerId,
237    ed25519_public_key_hex: String,
238    x25519_public_key_hex: String,
239}
240
241// Manual Debug impl that skips secret fields for security
242impl std::fmt::Debug for AgentIdentity {
243    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
244        f.debug_struct("AgentIdentity")
245            .field("peer_id", &self.peer_id)
246            .field("ed25519_public_key_hex", &self.ed25519_public_key_hex)
247            .field("x25519_public_key_hex", &self.x25519_public_key_hex)
248            .finish_non_exhaustive()
249    }
250}
251
252impl AgentIdentity {
253    /// Load agent identity from storage, migrating or generating as needed.
254    ///
255    /// Priority:
256    /// 1. Load from `agent.toml` (new consolidated format)
257    /// 2. Migrate from `agent_id.key` + separate files (legacy format)
258    /// 3. Generate new identity and save as `agent.toml`
259    pub fn from_storage_or_generate_new(storage_path: &Path) -> Result<Self> {
260        let toml_path = storage_path.join(AGENT_TOML_FILE);
261        let legacy_path = storage_path.join(LEGACY_KEY_FILE);
262
263        let (ed25519_keypair, x25519_secret, x25519_public) = if toml_path.exists() {
264            Self::load_keypairs_from_toml(&toml_path)?
265        } else if legacy_path.exists() {
266            Self::migrate_legacy_to_toml(storage_path)?
267        } else {
268            Self::generate_and_save_new_toml(storage_path)?
269        };
270
271        let peer_id = PeerId::from_public_key(&ed25519_keypair.public());
272        let ed25519_public_key_hex = hex::encode(ed25519_keypair.public().encode_protobuf());
273        let x25519_public_key_hex = hex::encode(x25519_public.as_bytes());
274
275        Ok(Self {
276            ed25519_keypair,
277            x25519_secret,
278            x25519_public,
279            peer_id,
280            ed25519_public_key_hex,
281            x25519_public_key_hex,
282        })
283    }
284
285    /// Legacy method for backwards compatibility - delegates to from_storage_or_generate_new
286    #[deprecated(note = "Use from_storage_or_generate_new instead")]
287    pub fn from_file_or_generate_new(path: &Path) -> Result<Self> {
288        // Assume path is to agent_id.key, get parent directory
289        let storage_path = path.parent().ok_or_else(|| {
290            Error::Config("Invalid identity path - no parent directory".to_string())
291        })?;
292        Self::from_storage_or_generate_new(storage_path)
293    }
294
295    pub fn peer_id(&self) -> &PeerId {
296        &self.peer_id
297    }
298
299    pub fn ed25519_public_key_hex(&self) -> &str {
300        &self.ed25519_public_key_hex
301    }
302
303    pub fn x25519_public_key_hex(&self) -> &str {
304        &self.x25519_public_key_hex
305    }
306
307    pub fn sign_message_as_hex(&self, message: &[u8]) -> Result<String> {
308        let signature = self
309            .ed25519_keypair
310            .sign(message)
311            .map_err(|e| Self::err("sign", e))?;
312        Ok(hex::encode(signature))
313    }
314
315    /// Decrypt data using x25519 + XChaCha20-Poly1305
316    ///
317    /// **Note:** This method is reserved for future encrypted communication features
318    /// (e.g., encrypted config updates, encrypted metrics). Currently unused but
319    /// maintained for forward compatibility with the encryption package.
320    ///
321    /// Expected format of encrypted_data:
322    /// - First 32 bytes: ephemeral x25519 public key
323    /// - Remaining bytes: XChaCha20-Poly1305 ciphertext (includes auth tag)
324    #[allow(dead_code)]
325    pub fn decrypt_x25519(
326        &self,
327        encrypted_data: &[u8],
328        nonce: &[u8],
329    ) -> std::result::Result<Vec<u8>, String> {
330        use chacha20poly1305::{
331            aead::{Aead, KeyInit},
332            XChaCha20Poly1305, XNonce,
333        };
334
335        // Extract ephemeral public key (first 32 bytes)
336        if encrypted_data.len() < 32 {
337            return Err("encrypted data too short".to_string());
338        }
339
340        let (ephemeral_public_bytes, ciphertext) = encrypted_data.split_at(32);
341        let ephemeral_public_array: [u8; 32] = ephemeral_public_bytes
342            .try_into()
343            .map_err(|_| "invalid ephemeral public key")?;
344        let ephemeral_public = X25519PublicKey::from(ephemeral_public_array);
345
346        // Compute shared secret using Diffie-Hellman
347        let shared_secret = self.x25519_secret.diffie_hellman(&ephemeral_public);
348
349        // Use shared secret as XChaCha20-Poly1305 key (24-byte nonce)
350        let cipher = XChaCha20Poly1305::new(shared_secret.as_bytes().into());
351
352        // Decrypt with provided 24-byte nonce
353        let nonce_array: XNonce = nonce
354            .try_into()
355            .map_err(|_| "Invalid nonce length".to_string())?;
356        let plaintext = cipher
357            .decrypt(&nonce_array, ciphertext)
358            .map_err(|e| format!("decryption failed: {}", e))?;
359
360        Ok(plaintext)
361    }
362
363    /// Load keypairs from TOML format (new consolidated format)
364    fn load_keypairs_from_toml(
365        toml_path: &Path,
366    ) -> Result<(Keypair, X25519SecretKey, X25519PublicKey)> {
367        tracing::info!("Loading agent identity from {}", toml_path.display());
368
369        let toml_content =
370            fs::read_to_string(toml_path).map_err(|e| Self::err("read agent.toml", e))?;
371        let agent_toml: AgentToml =
372            toml::from_str(&toml_content).map_err(|e| Self::err("parse agent.toml", e))?;
373
374        // Decode Ed25519 secret from base64
375        let ed25519_bytes = BASE64
376            .decode(&agent_toml.identity.ed25519_secret)
377            .map_err(|e| Self::err("decode Ed25519 base64", e))?;
378        let mut ed25519_bytes_array: [u8; 64] = ed25519_bytes
379            .try_into()
380            .map_err(|_| Self::err("invalid Ed25519 key length", "expected 64 bytes"))?;
381        let ed25519_kp = ed25519::Keypair::try_from_bytes(&mut ed25519_bytes_array)
382            .map_err(|e| Self::err("decode Ed25519 keypair", e))?;
383
384        // Decode X25519 secret from base64
385        let x25519_bytes = BASE64
386            .decode(&agent_toml.identity.x25519_secret)
387            .map_err(|e| Self::err("decode X25519 base64", e))?;
388        let x25519_bytes_array: [u8; 32] = x25519_bytes
389            .try_into()
390            .map_err(|_| Self::err("invalid X25519 key length", "expected 32 bytes"))?;
391        let x25519_secret = X25519SecretKey::from(x25519_bytes_array);
392        let x25519_public = X25519PublicKey::from(&x25519_secret);
393
394        Ok((ed25519_kp.into(), x25519_secret, x25519_public))
395    }
396
397    /// Migrate from legacy format (agent_id.key + separate files) to TOML
398    fn migrate_legacy_to_toml(
399        storage_path: &Path,
400    ) -> Result<(Keypair, X25519SecretKey, X25519PublicKey)> {
401        let legacy_path = storage_path.join(LEGACY_KEY_FILE);
402        tracing::info!(
403            "Migrating legacy agent identity from {} to agent.toml",
404            legacy_path.display()
405        );
406
407        // Load keys from legacy format (JSON or binary)
408        let (ed25519_kp, x25519_secret, x25519_public) =
409            Self::load_legacy_keypairs(&legacy_path)?;
410
411        // Read optional registration data from separate files
412        let claim_token = Self::read_legacy_file(storage_path, LEGACY_CLAIM_TOKEN_FILE);
413        let name = Self::read_legacy_file(storage_path, LEGACY_AGENT_NAME_FILE);
414
415        // Build and save TOML
416        let agent_toml = AgentToml {
417            identity: IdentitySection {
418                ed25519_secret: BASE64.encode(ed25519_kp.to_bytes()),
419                x25519_secret: BASE64.encode(x25519_secret.to_bytes()),
420            },
421            registration: if name.is_some() || claim_token.is_some() {
422                Some(RegistrationSection { name, claim_token })
423            } else {
424                None
425            },
426        };
427
428        let toml_path = storage_path.join(AGENT_TOML_FILE);
429        let toml_content =
430            toml::to_string_pretty(&agent_toml).map_err(|e| Self::err("serialize agent.toml", e))?;
431        fs::write(&toml_path, &toml_content).map_err(|e| Self::err("write agent.toml", e))?;
432
433        tracing::info!(
434            "Migration complete - agent data consolidated in {}",
435            toml_path.display()
436        );
437
438        Ok((ed25519_kp.into(), x25519_secret, x25519_public))
439    }
440
441    /// Load keypairs from legacy JSON format (agent_id.key)
442    fn load_legacy_keypairs(
443        path: &Path,
444    ) -> Result<(ed25519::Keypair, X25519SecretKey, X25519PublicKey)> {
445        // Try loading as JSON first
446        if let Ok(json) = fs::read_to_string(path) {
447            if let Ok(persisted) = serde_json::from_str::<PersistedKeys>(&json) {
448                let mut ed25519_bytes = persisted.ed25519_bytes;
449                let ed25519_kp = ed25519::Keypair::try_from_bytes(&mut ed25519_bytes)
450                    .map_err(|e| Self::err("decode Ed25519 key", e))?;
451
452                let x25519_bytes: [u8; 32] = persisted
453                    .x25519_bytes
454                    .try_into()
455                    .map_err(|_| Self::err("invalid X25519 key length", "expected 32 bytes"))?;
456                let x25519_secret = X25519SecretKey::from(x25519_bytes);
457                let x25519_public = X25519PublicKey::from(&x25519_secret);
458
459                return Ok((ed25519_kp, x25519_secret, x25519_public));
460            }
461        }
462
463        // Fall back to old binary format (ed25519 only, needs X25519 generation)
464        tracing::info!("Loading old binary identity format");
465        let mut bytes = fs::read(path).map_err(|e| Self::err("read identity file", e))?;
466        let ed25519_kp = ed25519::Keypair::try_from_bytes(&mut bytes)
467            .map_err(|e| Self::err("decode Ed25519 key", e))?;
468
469        // Generate new X25519 keypair for migration
470        let x25519_secret = X25519SecretKey::random_from_rng(OsRng);
471        let x25519_public = X25519PublicKey::from(&x25519_secret);
472
473        Ok((ed25519_kp, x25519_secret, x25519_public))
474    }
475
476    /// Read a legacy file (claim_token or agent_name), returning None if missing/empty
477    fn read_legacy_file(storage_path: &Path, filename: &str) -> Option<String> {
478        let path = storage_path.join(filename);
479        if path.exists() {
480            fs::read_to_string(&path)
481                .ok()
482                .map(|s| s.trim().to_string())
483                .filter(|s| !s.is_empty())
484        } else {
485            None
486        }
487    }
488
489    /// Generate new identity and save as TOML
490    fn generate_and_save_new_toml(
491        storage_path: &Path,
492    ) -> Result<(Keypair, X25519SecretKey, X25519PublicKey)> {
493        let toml_path = storage_path.join(AGENT_TOML_FILE);
494        tracing::info!(
495            "Generating new agent identity (saved to {})",
496            toml_path.display()
497        );
498
499        // Generate Ed25519 keypair
500        let mut bytes = [0u8; 32];
501        use rand::RngCore;
502        OsRng.fill_bytes(&mut bytes);
503        let ed25519_kp = ed25519::Keypair::from(
504            ed25519::SecretKey::try_from_bytes(&mut bytes)
505                .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e.to_string()))?,
506        );
507
508        // Generate X25519 keypair
509        let x25519_secret = X25519SecretKey::random_from_rng(OsRng);
510        let x25519_public = X25519PublicKey::from(&x25519_secret);
511
512        // Build TOML (no registration data for new identity)
513        let agent_toml = AgentToml {
514            identity: IdentitySection {
515                ed25519_secret: BASE64.encode(ed25519_kp.to_bytes()),
516                x25519_secret: BASE64.encode(x25519_secret.to_bytes()),
517            },
518            registration: None,
519        };
520
521        // Ensure storage directory exists
522        fs::create_dir_all(storage_path).map_err(|e| Self::err("create storage directory", e))?;
523
524        let toml_content =
525            toml::to_string_pretty(&agent_toml).map_err(|e| Self::err("serialize agent.toml", e))?;
526        fs::write(&toml_path, &toml_content).map_err(|e| Self::err("write agent.toml", e))?;
527
528        Ok((ed25519_kp.into(), x25519_secret, x25519_public))
529    }
530
531    fn err(context: &str, error: impl std::fmt::Display) -> Error {
532        Error::Io(std::io::Error::new(
533            std::io::ErrorKind::Other,
534            format!("{}: {}", context, error),
535        ))
536    }
537}
538
539// ============================================================================
540// PUBLIC HELPERS
541// ============================================================================
542
543/// Update the registration section of agent.toml
544///
545/// This is used by the AlertsWorker to persist name and claim_token after
546/// successful registration with the server.
547///
548/// If agent.toml doesn't exist, this will return an error - the agent must
549/// be initialized first via AgentIdentity::from_storage_or_generate_new().
550pub fn update_agent_toml_registration(
551    storage_path: &Path,
552    name: Option<&str>,
553    claim_token: Option<&str>,
554) -> Result<()> {
555    let toml_path = storage_path.join(AGENT_TOML_FILE);
556
557    if !toml_path.exists() {
558        return Err(Error::Config(format!(
559            "Cannot update registration: {} does not exist",
560            toml_path.display()
561        )));
562    }
563
564    // Read existing TOML
565    let toml_content =
566        fs::read_to_string(&toml_path).map_err(|e| Error::Io(std::io::Error::new(
567            e.kind(),
568            format!("read agent.toml: {}", e),
569        )))?;
570    let mut agent_toml: AgentToml =
571        toml::from_str(&toml_content).map_err(|e| Error::Config(format!("parse agent.toml: {}", e)))?;
572
573    // Update registration section
574    let mut registration = agent_toml.registration.unwrap_or_default();
575
576    if let Some(n) = name {
577        registration.name = Some(n.to_string());
578    }
579    if let Some(ct) = claim_token {
580        registration.claim_token = Some(ct.to_string());
581    }
582
583    // Only set registration if there's something to store
584    agent_toml.registration = if registration.name.is_some() || registration.claim_token.is_some() {
585        Some(registration)
586    } else {
587        None
588    };
589
590    // Write back
591    let toml_content = toml::to_string_pretty(&agent_toml)
592        .map_err(|e| Error::Config(format!("serialize agent.toml: {}", e)))?;
593    fs::write(&toml_path, &toml_content).map_err(|e| Error::Io(std::io::Error::new(
594        e.kind(),
595        format!("write agent.toml: {}", e),
596    )))?;
597
598    tracing::debug!("Updated registration in {}", toml_path.display());
599    Ok(())
600}
601
602/// Read claim token from storage (TOML or legacy file)
603///
604/// Used by AlertsWorker during startup to load existing claim token.
605pub fn read_claim_token(storage_path: &Path) -> Option<String> {
606    let toml_path = storage_path.join(AGENT_TOML_FILE);
607
608    // Try TOML first
609    if toml_path.exists() {
610        if let Ok(toml_content) = fs::read_to_string(&toml_path) {
611            if let Ok(agent_toml) = toml::from_str::<AgentToml>(&toml_content) {
612                if let Some(reg) = agent_toml.registration {
613                    if reg.claim_token.is_some() {
614                        return reg.claim_token;
615                    }
616                }
617            }
618        }
619    }
620
621    // Fall back to legacy file
622    AgentIdentity::read_legacy_file(storage_path, LEGACY_CLAIM_TOKEN_FILE)
623}