Change SecretError::Resolve to preserve full error chain via Arc
Arc<dyn Error + Send + Sync> satisfies Clone without requiring the
inner error to be Clone. OnceLock in SecretSource::File now stores
Arc<dyn Error> instead of a formatted string, and quire-ci wraps
reqwest errors in Arc rather than stringifying them.

https://claude.ai/code/session_01GfccpUReesMY5Rb4FhajhT
change
commit 734a20ea98a87cca3254031fcc0e478895117e7a
author Claude <noreply@anthropic.com>
date
parent fc204484
diff --git a/quire-ci/src/main.rs b/quire-ci/src/main.rs
index 2218574..9700927 100644
--- a/quire-ci/src/main.rs
+++ b/quire-ci/src/main.rs
@@ -19,6 +19,7 @@ use quire_core::fennel::FennelError;
 use quire_core::secret::{Error as SecretError, Result as SecretResult, SecretRegistry};
 use quire_core::telemetry::{self, FmtMode, MietteLayer};
 use reqwest::header::{AUTHORIZATION, HeaderMap, HeaderValue};
+use std::sync::Arc;
 
 /// Errors from running a job's `run_fn`. Lua errors are re-wrapped
 /// via [`FennelError::from_lua`] so they carry the same source-code
@@ -255,15 +256,15 @@ impl RunClient {
     fn fetch_secret(&self, name: &str) -> SecretResult<String> {
         let resp = self
             .get(&format!("secrets/{name}"))
-            .map_err(|e| SecretError::Resolve(e.to_string()))?;
+            .map_err(|e| SecretError::Resolve(Arc::new(e)))?;
         if resp.status() == reqwest::StatusCode::NOT_FOUND {
             return Err(SecretError::UnknownSecret(name.to_string()));
         }
         resp.error_for_status()
-            .map_err(|e| SecretError::Resolve(e.to_string()))?
+            .map_err(|e| SecretError::Resolve(Arc::new(e)))?
             .json::<SecretResponse>()
             .map(|r| r.value)
-            .map_err(|e| SecretError::Resolve(e.to_string()))
+            .map_err(|e| SecretError::Resolve(Arc::new(e)))
     }
 }
 
diff --git a/quire-core/src/secret.rs b/quire-core/src/secret.rs
index ab6b826..73ffc33 100644
--- a/quire-core/src/secret.rs
+++ b/quire-core/src/secret.rs
@@ -1,18 +1,15 @@
 use std::collections::HashMap;
 use std::path::PathBuf;
-use std::sync::OnceLock;
+use std::sync::{Arc, OnceLock};
 
 /// Errors produced by secret resolution.
 #[derive(Debug, Clone, thiserror::Error)]
 pub enum Error {
-    /// File-backed secret could not be read.
-    ///
-    /// Stored as a string because `OnceLock` in `SecretString::reveal` caches
-    /// the error and `std::io::Error` is not `Clone`. Once `once_cell_try`
-    /// stabilizes (allowing `OnceLock::get_or_try_init` with a separate error
-    /// type), we can store a structured error instead of a string.
-    #[error("secret resolution failed: {0}")]
-    Resolve(String),
+    /// Secret could not be resolved. The source error is preserved for
+    /// diagnostics. `Arc` provides `Clone` without requiring the inner
+    /// error to be `Clone`.
+    #[error("secret resolution failed")]
+    Resolve(#[source] Arc<dyn std::error::Error + Send + Sync>),
 
     #[error("unknown secret: {0:?}")]
     UnknownSecret(String),
@@ -38,7 +35,7 @@ enum SecretSource {
     Plain(String),
     File {
         path: PathBuf,
-        resolved: OnceLock<std::result::Result<String, String>>,
+        resolved: OnceLock<std::result::Result<String, Arc<dyn std::error::Error + Send + Sync>>>,
     },
 }
 
@@ -60,12 +57,6 @@ impl SecretString {
     ///
     /// For the file variant, reads from disk on first call and caches the
     /// result. Errors are also cached — subsequent calls return the same error.
-    ///
-    /// The error is stored as a formatted string inside `OnceLock` because
-    /// `std::io::Error` is not `Clone`, and `OnceLock::get_or_init` requires
-    /// the closure output to be `Sized` + ownable. Once `once_cell_try`
-    /// stabilizes (allowing `OnceLock::get_or_try_init` with a separate error
-    /// type), we can store a structured error instead of a string.
     pub fn reveal(&self) -> Result<&str> {
         match &self.0 {
             SecretSource::Plain(s) => Ok(s.as_str()),
@@ -73,11 +64,11 @@ impl SecretString {
                 .get_or_init(|| {
                     fs_err::read_to_string(path)
                         .map(|s| s.strip_suffix('\n').unwrap_or(&s).to_string())
-                        .map_err(|e| format!("{}: {e}", path.display()))
+                        .map_err(|e| Arc::new(e) as Arc<dyn std::error::Error + Send + Sync>)
                 })
                 .as_ref()
                 .map(|s| s.as_str())
-                .map_err(|msg| Error::Resolve(msg.clone())),
+                .map_err(|arc| Error::Resolve(Arc::clone(arc))),
         }
     }
 }