Skip to main content

jacs_binding_core/
lib.rs

1//! # jacs-binding-core
2//!
3//! Shared core logic for JACS language bindings (Python, Node.js, etc.).
4//!
5//! This crate provides the binding-agnostic business logic that can be used
6//! by any language binding. Each binding implements the `BindingError` trait
7//! to convert errors to their native format.
8
9use base64::{Engine as _, engine::general_purpose};
10use jacs::agent::agreement::Agreement;
11use jacs::agent::boilerplate::BoilerPlate;
12use jacs::agent::document::{DocumentTraits, JACSDocument};
13use jacs::agent::payloads::PayloadTraits;
14use jacs::agent::{
15    AGENT_AGREEMENT_FIELDNAME, AGENT_REGISTRATION_SIGNATURE_FIELDNAME, AGENT_SIGNATURE_FIELDNAME,
16    Agent,
17};
18use jacs::config::Config;
19use jacs::crypt::KeyManager;
20use jacs::crypt::hash::hash_string as jacs_hash_string;
21use reqwest::blocking::Client as BlockingClient;
22use reqwest::header::{ACCEPT, CONTENT_TYPE};
23use reqwest::{StatusCode, Url};
24use serde_json::{Value, json};
25use std::collections::HashMap;
26use std::fs;
27use std::path::{Path, PathBuf};
28use std::str::FromStr;
29use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
30
31pub mod conversion;
32pub mod doc_wrapper;
33pub mod simple_wrapper;
34
35pub use doc_wrapper::DocumentServiceWrapper;
36pub use simple_wrapper::SimpleAgentWrapper;
37pub use simple_wrapper::sign_message_json;
38pub use simple_wrapper::verify_json;
39
40/// Error type for binding core operations.
41///
42/// This is the internal error type that binding implementations convert
43/// to their native error types (PyErr, napi::Error, etc.).
44#[derive(Debug)]
45pub struct BindingCoreError {
46    pub message: String,
47    pub kind: ErrorKind,
48}
49
50/// Categories of errors for better handling by bindings.
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub enum ErrorKind {
53    /// Failed to acquire a mutex lock
54    LockFailed,
55    /// Agent loading or configuration failed
56    AgentLoad,
57    /// Validation failed (agent or document)
58    Validation,
59    /// Signature operation failed
60    SigningFailed,
61    /// Verification operation failed
62    VerificationFailed,
63    /// Document operation failed
64    DocumentFailed,
65    /// Agreement operation failed
66    AgreementFailed,
67    /// Serialization/deserialization failed
68    SerializationFailed,
69    /// Invalid argument provided
70    InvalidArgument,
71    /// Trust store operation failed
72    TrustFailed,
73    /// Network operation failed
74    NetworkFailed,
75    /// Key not found
76    KeyNotFound,
77    /// Generic failure
78    Generic,
79}
80
81impl BindingCoreError {
82    pub fn new(kind: ErrorKind, message: impl Into<String>) -> Self {
83        Self {
84            message: message.into(),
85            kind,
86        }
87    }
88
89    pub fn lock_failed(message: impl Into<String>) -> Self {
90        Self::new(ErrorKind::LockFailed, message)
91    }
92
93    pub fn agent_load(message: impl Into<String>) -> Self {
94        Self::new(ErrorKind::AgentLoad, message)
95    }
96
97    pub fn validation(message: impl Into<String>) -> Self {
98        Self::new(ErrorKind::Validation, message)
99    }
100
101    pub fn signing_failed(message: impl Into<String>) -> Self {
102        Self::new(ErrorKind::SigningFailed, message)
103    }
104
105    pub fn verification_failed(message: impl Into<String>) -> Self {
106        Self::new(ErrorKind::VerificationFailed, message)
107    }
108
109    pub fn document_failed(message: impl Into<String>) -> Self {
110        Self::new(ErrorKind::DocumentFailed, message)
111    }
112
113    pub fn agreement_failed(message: impl Into<String>) -> Self {
114        Self::new(ErrorKind::AgreementFailed, message)
115    }
116
117    pub fn serialization_failed(message: impl Into<String>) -> Self {
118        Self::new(ErrorKind::SerializationFailed, message)
119    }
120
121    pub fn invalid_argument(message: impl Into<String>) -> Self {
122        Self::new(ErrorKind::InvalidArgument, message)
123    }
124
125    pub fn trust_failed(message: impl Into<String>) -> Self {
126        Self::new(ErrorKind::TrustFailed, message)
127    }
128
129    pub fn network_failed(message: impl Into<String>) -> Self {
130        Self::new(ErrorKind::NetworkFailed, message)
131    }
132
133    pub fn key_not_found(message: impl Into<String>) -> Self {
134        Self::new(ErrorKind::KeyNotFound, message)
135    }
136
137    pub fn generic(message: impl Into<String>) -> Self {
138        Self::new(ErrorKind::Generic, message)
139    }
140}
141
142impl std::fmt::Display for BindingCoreError {
143    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
144        write!(f, "{}", self.message)
145    }
146}
147
148impl std::error::Error for BindingCoreError {}
149
150impl<T> From<PoisonError<T>> for BindingCoreError {
151    fn from(e: PoisonError<T>) -> Self {
152        Self::lock_failed(format!("Failed to acquire lock: {}", e))
153    }
154}
155
156/// Result type for binding core operations.
157pub type BindingResult<T> = Result<T, BindingCoreError>;
158
159fn serialize_agent_info(info: &jacs::simple::AgentInfo) -> BindingResult<String> {
160    serde_json::to_string(info).map_err(|e| {
161        BindingCoreError::serialization_failed(format!("Failed to serialize AgentInfo: {}", e))
162    })
163}
164
165fn resolve_existing_config_path(config_path: &str) -> BindingResult<String> {
166    let requested = Path::new(config_path);
167    let resolved = if requested.is_absolute() {
168        requested.to_path_buf()
169    } else {
170        std::env::current_dir()
171            .map_err(|e| {
172                BindingCoreError::agent_load(format!(
173                    "Failed to determine current working directory: {}",
174                    e
175                ))
176            })?
177            .join(requested)
178    };
179
180    if !resolved.exists() {
181        return Err(BindingCoreError::agent_load(format!(
182            "Config file not found: {}",
183            resolved.display()
184        )));
185    }
186
187    Ok(normalize_path(&resolved).to_string_lossy().into_owned())
188}
189
190fn normalize_path(path: &Path) -> PathBuf {
191    let mut normalized = PathBuf::new();
192    for component in path.components() {
193        match component {
194            std::path::Component::CurDir => {}
195            std::path::Component::ParentDir => {
196                normalized.pop();
197            }
198            other => normalized.push(other.as_os_str()),
199        }
200    }
201    normalized
202}
203
204fn resolve_path_from_cwd(path: &str) -> BindingResult<PathBuf> {
205    let requested = Path::new(path);
206    if requested.is_absolute() {
207        return Ok(normalize_path(requested));
208    }
209
210    Ok(normalize_path(
211        &std::env::current_dir()
212            .map_err(|e| {
213                BindingCoreError::agent_load(format!(
214                    "Failed to determine current working directory: {}",
215                    e
216                ))
217            })?
218            .join(requested),
219    ))
220}
221
222fn resolve_relative_to_config(config_path: &Path, candidate: &str) -> PathBuf {
223    let candidate_path = Path::new(candidate);
224    if candidate_path.is_absolute() {
225        return normalize_path(candidate_path);
226    }
227
228    let base_dir = config_path.parent().unwrap_or_else(|| Path::new("."));
229    normalize_path(&base_dir.join(candidate_path))
230}
231
232fn read_password_file(path: &Path) -> BindingResult<Option<String>> {
233    if !path.exists() {
234        return Ok(None);
235    }
236
237    let contents = fs::read_to_string(path).map_err(|e| {
238        BindingCoreError::generic(format!(
239            "Failed to read password file {}: {}",
240            path.display(),
241            e
242        ))
243    })?;
244    let password = contents.trim_end_matches(|c| c == '\n' || c == '\r').trim();
245    if password.is_empty() {
246        return Ok(None);
247    }
248    Ok(Some(password.to_string()))
249}
250
251fn missing_password_message(error: &str) -> bool {
252    error.contains("No private key password available")
253}
254
255fn truthy_env_var(name: &str) -> bool {
256    std::env::var(name)
257        .ok()
258        .map(|value| {
259            let value = value.trim();
260            value.eq_ignore_ascii_case("true") || value == "1"
261        })
262        .unwrap_or(false)
263}
264
265const DEFAULT_NETWORK_TIMEOUT_MS: u64 = 10_000;
266const DEFAULT_KEYS_BASE_URL: &str = "https://hai.ai";
267
268fn build_blocking_json_client(timeout_ms: u64) -> BindingResult<BlockingClient> {
269    BlockingClient::builder()
270        .timeout(std::time::Duration::from_millis(timeout_ms.max(1)))
271        .build()
272        .map_err(|e| {
273            BindingCoreError::network_failed(format!("Failed to build HTTP client: {}", e))
274        })
275}
276
277fn is_loopback_host(host: &str) -> bool {
278    matches!(
279        host.trim().trim_matches(['[', ']']),
280        "localhost" | "127.0.0.1" | "::1"
281    )
282}
283
284fn validate_network_url(url: &Url, description: &str) -> BindingResult<()> {
285    match url.scheme() {
286        "https" => Ok(()),
287        "http" if url.host_str().is_some_and(is_loopback_host) => Ok(()),
288        "http" => Err(BindingCoreError::network_failed(format!(
289            "{} must use HTTPS (got '{}'). Only localhost URLs are allowed over HTTP for testing.",
290            description, url
291        ))),
292        other => Err(BindingCoreError::invalid_argument(format!(
293            "{} must use http or https (got scheme '{}')",
294            description, other
295        ))),
296    }
297}
298
299fn content_type_header(response: &reqwest::blocking::Response) -> String {
300    response
301        .headers()
302        .get(CONTENT_TYPE)
303        .and_then(|value| value.to_str().ok())
304        .unwrap_or("")
305        .to_string()
306}
307
308fn parse_json_object_body(
309    body: &str,
310    invalid_json_message: String,
311    non_object_message: String,
312) -> BindingResult<String> {
313    let value: Value = serde_json::from_str(body)
314        .map_err(|e| BindingCoreError::validation(format!("{}: {}", invalid_json_message, e)))?;
315    if !value.is_object() {
316        return Err(BindingCoreError::validation(non_object_message));
317    }
318    serde_json::to_string(&value).map_err(|e| {
319        BindingCoreError::serialization_failed(format!("Failed to serialize JSON response: {}", e))
320    })
321}
322
323fn resolve_keys_base_url(override_base_url: Option<&str>) -> String {
324    if let Some(value) = override_base_url {
325        let trimmed = value.trim();
326        if !trimmed.is_empty() {
327            return trimmed.trim_end_matches('/').to_string();
328        }
329    }
330
331    if let Ok(value) = std::env::var("JACS_KEYS_BASE_URL") {
332        let trimmed = value.trim();
333        if !trimmed.is_empty() {
334            return trimmed.trim_end_matches('/').to_string();
335        }
336    }
337
338    if let Ok(value) = std::env::var("HAI_KEYS_BASE_URL") {
339        let trimmed = value.trim();
340        if !trimmed.is_empty() {
341            return trimmed.trim_end_matches('/').to_string();
342        }
343    }
344    DEFAULT_KEYS_BASE_URL.to_string()
345}
346
347fn normalize_public_key_hash(public_key_hash: &str) -> BindingResult<String> {
348    let trimmed = public_key_hash.trim();
349    if trimmed.is_empty() {
350        return Err(BindingCoreError::invalid_argument(
351            "public_key_hash cannot be empty",
352        ));
353    }
354    if trimmed.starts_with("sha256:") {
355        Ok(trimmed.to_string())
356    } else {
357        Ok(format!("sha256:{}", trimmed))
358    }
359}
360
361fn decode_public_key_base64(public_key_b64: &str) -> BindingResult<Vec<u8>> {
362    for engine in [
363        &general_purpose::STANDARD,
364        &general_purpose::STANDARD_NO_PAD,
365        &general_purpose::URL_SAFE,
366        &general_purpose::URL_SAFE_NO_PAD,
367    ] {
368        if let Ok(decoded) = engine.decode(public_key_b64) {
369            return Ok(decoded);
370        }
371    }
372
373    Err(BindingCoreError::invalid_argument(
374        "Public key must be valid base64 or base64url text.",
375    ))
376}
377
378fn build_jwk_set_from_public_key_bytes(
379    public_key: &[u8],
380    key_algorithm: &str,
381    key_id: &str,
382) -> BindingResult<Value> {
383    let normalized_algorithm = key_algorithm.trim().to_ascii_lowercase();
384
385    if normalized_algorithm.contains("ed25519")
386        || (normalized_algorithm.is_empty() && public_key.len() == 32)
387    {
388        if public_key.len() != 32 {
389            return Err(BindingCoreError::invalid_argument(format!(
390                "Ed25519 public key must be 32 bytes, got {} bytes.",
391                public_key.len()
392            )));
393        }
394
395        return Ok(json!({
396            "keys": [{
397                "kty": "OKP",
398                "crv": "Ed25519",
399                "x": general_purpose::URL_SAFE_NO_PAD.encode(public_key),
400                "kid": key_id,
401                "use": "sig",
402                "alg": "EdDSA",
403            }]
404        }));
405    }
406
407    if normalized_algorithm.contains("rsa") || normalized_algorithm.is_empty() {
408        use rsa::traits::PublicKeyParts;
409        use rsa::{RsaPublicKey, pkcs1::DecodeRsaPublicKey, pkcs8::DecodePublicKey};
410
411        let rsa_key = if let Ok(key) = RsaPublicKey::from_pkcs1_der(public_key) {
412            key
413        } else if let Ok(key) = RsaPublicKey::from_public_key_der(public_key) {
414            key
415        } else if let Ok(pem) = std::str::from_utf8(public_key) {
416            match RsaPublicKey::from_public_key_pem(pem) {
417                Ok(key) => key,
418                Err(e) if normalized_algorithm.contains("rsa") => {
419                    return Err(BindingCoreError::invalid_argument(format!(
420                        "Failed to parse RSA public key for JWK export: {}",
421                        e
422                    )));
423                }
424                Err(_) => return Ok(json!({ "keys": [] })),
425            }
426        } else if normalized_algorithm.contains("rsa") {
427            return Err(BindingCoreError::invalid_argument(
428                "Failed to parse RSA public key for JWK export.",
429            ));
430        } else {
431            return Ok(json!({ "keys": [] }));
432        };
433
434        return Ok(json!({
435            "keys": [{
436                "kty": "RSA",
437                "kid": key_id,
438                "alg": "RS256",
439                "use": "sig",
440                "n": general_purpose::URL_SAFE_NO_PAD.encode(rsa_key.n().to_bytes_be()),
441                "e": general_purpose::URL_SAFE_NO_PAD.encode(rsa_key.e().to_bytes_be()),
442            }]
443        }));
444    }
445
446    Ok(json!({ "keys": [] }))
447}
448
449fn resolve_password_context(
450    config_path: Option<&str>,
451    key_directory: Option<&str>,
452) -> BindingResult<(PathBuf, Option<String>)> {
453    let mut agent_id = None;
454
455    if let Some(config_path) = config_path {
456        let resolved_config_path = resolve_path_from_cwd(config_path)?;
457        if resolved_config_path.exists() {
458            let config = Config::from_file(resolved_config_path.to_string_lossy().as_ref())
459                .map_err(|e| {
460                    BindingCoreError::agent_load(format!(
461                        "Failed to load config from {}: {}",
462                        resolved_config_path.display(),
463                        e
464                    ))
465                })?;
466            let configured_key_dir = config
467                .jacs_key_directory()
468                .as_deref()
469                .unwrap_or("./jacs_keys");
470            let resolved_key_dir =
471                resolve_relative_to_config(&resolved_config_path, configured_key_dir);
472            agent_id = config.jacs_agent_id_and_version().clone();
473
474            if let Some(key_directory) = key_directory {
475                return Ok((resolve_path_from_cwd(key_directory)?, agent_id));
476            }
477            return Ok((resolved_key_dir, agent_id));
478        }
479
480        if let Some(key_directory) = key_directory {
481            return Ok((resolve_path_from_cwd(key_directory)?, None));
482        }
483
484        return Ok((
485            resolve_relative_to_config(&resolved_config_path, "./jacs_keys"),
486            None,
487        ));
488    }
489
490    if let Some(key_directory) = key_directory {
491        return Ok((resolve_path_from_cwd(key_directory)?, None));
492    }
493
494    Ok((resolve_path_from_cwd("./jacs_keys")?, agent_id))
495}
496
497fn generate_private_key_password_value() -> String {
498    use rand::Rng;
499
500    const UPPER: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ";
501    const LOWER: &[u8] = b"abcdefghijklmnopqrstuvwxyz";
502    const DIGITS: &[u8] = b"0123456789";
503    const SPECIAL: &[u8] = b"!@#$%^&*()-_=+";
504    const ALL: &[u8] =
505        b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()-_=+";
506
507    let mut rng = rand::rng();
508    let mut password = String::with_capacity(32);
509
510    password.push(UPPER[rng.random_range(0..UPPER.len())] as char);
511    password.push(LOWER[rng.random_range(0..LOWER.len())] as char);
512    password.push(DIGITS[rng.random_range(0..DIGITS.len())] as char);
513    password.push(SPECIAL[rng.random_range(0..SPECIAL.len())] as char);
514
515    for _ in 4..32 {
516        password.push(ALL[rng.random_range(0..ALL.len())] as char);
517    }
518
519    password
520}
521
522fn persist_password_file(key_directory: &Path, password: &str) -> BindingResult<()> {
523    fs::create_dir_all(key_directory).map_err(|e| {
524        BindingCoreError::generic(format!(
525            "Failed to create key directory {}: {}",
526            key_directory.display(),
527            e
528        ))
529    })?;
530
531    let password_path = key_directory.join(".jacs_password");
532    fs::write(&password_path, password).map_err(|e| {
533        BindingCoreError::generic(format!(
534            "Failed to write password file {}: {}",
535            password_path.display(),
536            e
537        ))
538    })?;
539
540    #[cfg(unix)]
541    {
542        use std::os::unix::fs::PermissionsExt;
543
544        let permissions = std::fs::Permissions::from_mode(0o600);
545        fs::set_permissions(&password_path, permissions).map_err(|e| {
546            BindingCoreError::generic(format!(
547                "Failed to set password file permissions on {}: {}",
548                password_path.display(),
549                e
550            ))
551        })?;
552    }
553
554    Ok(())
555}
556
557fn is_editable_level(level: &str) -> bool {
558    matches!(level, "artifact" | "config")
559}
560
561fn normalize_agent_id_for_compare(agent_id: &str) -> &str {
562    agent_id.split(':').next().unwrap_or(agent_id)
563}
564
565fn extract_agreement_payload(value: &Value) -> Value {
566    if let Some(payload) = value.get("jacsDocument") {
567        return payload.clone();
568    }
569    if let Some(payload) = value.get("content") {
570        return payload.clone();
571    }
572    if let Some(obj) = value.as_object() {
573        let mut filtered = serde_json::Map::new();
574        for (k, v) in obj {
575            if !k.starts_with("jacs") && k != "$schema" {
576                filtered.insert(k.clone(), v.clone());
577            }
578        }
579        if !filtered.is_empty() {
580            return Value::Object(filtered);
581        }
582    }
583    Value::Null
584}
585
586fn create_editable_agreement_document(
587    agent: &mut Agent,
588    payload: Value,
589) -> BindingResult<JACSDocument> {
590    let wrapped = json!({
591        "jacsType": "artifact",
592        "jacsLevel": "artifact",
593        "content": payload
594    });
595    agent
596        .create_document_and_load(&wrapped.to_string(), None, None)
597        .map_err(|e| {
598            BindingCoreError::document_failed(format!(
599                "Failed to create editable agreement document: {}",
600                e
601            ))
602        })
603}
604
605fn ensure_editable_agreement_document(
606    agent: &mut Agent,
607    document_string: &str,
608) -> BindingResult<JACSDocument> {
609    match agent.load_document(document_string) {
610        Ok(doc) => {
611            let level = doc
612                .value
613                .get("jacsLevel")
614                .and_then(|v| v.as_str())
615                .unwrap_or("");
616            if is_editable_level(level) {
617                Ok(doc)
618            } else {
619                let payload = extract_agreement_payload(doc.getvalue());
620                create_editable_agreement_document(agent, payload)
621            }
622        }
623        Err(load_err) => {
624            if let Ok(parsed) = serde_json::from_str::<Value>(document_string)
625                && (parsed.get("jacsId").is_some() || parsed.get("jacsVersion").is_some())
626            {
627                return Err(BindingCoreError::document_failed(format!(
628                    "Failed to load document: {}",
629                    load_err
630                )));
631            }
632            let payload = serde_json::from_str::<Value>(document_string)
633                .unwrap_or_else(|_| Value::String(document_string.to_string()));
634            create_editable_agreement_document(agent, payload)
635        }
636    }
637}
638
639// =============================================================================
640// Wrapper Type for Agent with Arc<Mutex<Agent>>
641// =============================================================================
642
643/// Thread-safe wrapper around a JACS Agent.
644///
645/// This provides the core agent functionality that all bindings share.
646/// Bindings wrap this in their own types and convert errors appropriately.
647#[derive(Clone)]
648pub struct AgentWrapper {
649    inner: Arc<Mutex<Agent>>,
650    private_key_password: Arc<Mutex<Option<String>>>,
651}
652
653// ScopedPrivateKeyEnv and private_key_env_lock removed:
654// Password is now set directly on Agent.password (agent-scoped, no global state).
655// See ENV_SECURITY_PRD Task 006.
656
657impl Default for AgentWrapper {
658    fn default() -> Self {
659        Self::new()
660    }
661}
662
663impl AgentWrapper {
664    /// Create a new empty agent wrapper.
665    pub fn new() -> Self {
666        Self {
667            inner: Arc::new(Mutex::new(jacs::get_empty_agent())),
668            private_key_password: Arc::new(Mutex::new(None)),
669        }
670    }
671
672    /// Create an agent wrapper from an existing Arc<Mutex<Agent>>.
673    ///
674    /// This is used by the Go FFI to share the agent handle's inner agent
675    /// with binding-core's attestation methods.
676    pub fn from_inner(inner: Arc<Mutex<Agent>>) -> Self {
677        Self {
678            inner,
679            private_key_password: Arc::new(Mutex::new(None)),
680        }
681    }
682
683    /// Get the inner `Arc<Mutex<Agent>>`.
684    ///
685    /// Used to share the agent handle with `DocumentServiceWrapper` and other
686    /// components that need direct access to the underlying agent.
687    pub fn inner_arc(&self) -> Arc<Mutex<Agent>> {
688        Arc::clone(&self.inner)
689    }
690
691    /// Get a locked reference to the inner agent.
692    fn lock(&self) -> BindingResult<MutexGuard<'_, Agent>> {
693        self.inner.lock().map_err(BindingCoreError::from)
694    }
695
696    fn configured_private_key_password(&self) -> BindingResult<Option<String>> {
697        self.private_key_password
698            .lock()
699            .map_err(BindingCoreError::from)
700            .map(|password| password.clone())
701    }
702
703    fn with_private_key_password<T>(
704        &self,
705        operation: impl FnOnce() -> BindingResult<T>,
706    ) -> BindingResult<T> {
707        // Always sync the wrapper's password state to the Agent's agent-scoped
708        // password field, including None. This ensures that when a caller clears
709        // the wrapper password, the inner Agent also has its password cleared
710        // so it falls back to env/jenv/keychain resolution (Issue 013).
711        {
712            let password = self.configured_private_key_password()?;
713            let mut agent = self.lock()?;
714            agent.set_password(password);
715        }
716        operation()
717    }
718
719    /// Configure a per-wrapper private-key password for load/sign operations.
720    ///
721    /// This lets higher-level bindings keep per-instance passwords out of
722    /// process-global environment management while the current core library
723    /// still resolves decryption passwords through `JACS_PRIVATE_KEY_PASSWORD`.
724    pub fn set_private_key_password(&self, password: Option<String>) -> BindingResult<()> {
725        let mut slot = self
726            .private_key_password
727            .lock()
728            .map_err(BindingCoreError::from)?;
729        *slot = password.and_then(|value| if value.is_empty() { None } else { Some(value) });
730        Ok(())
731    }
732
733    /// Load agent configuration from a file path.
734    ///
735    /// Uses `Config::from_file` + `apply_env_overrides` + `Agent::from_config`
736    /// to avoid deprecated `load_by_config` and env var side-channels.
737    pub fn load(&self, config_path: String) -> BindingResult<String> {
738        let password = self.configured_private_key_password()?;
739        let new_agent = self.load_agent_from_config(&config_path, true, password.as_deref())?;
740        *self.lock()? = new_agent;
741        Ok("Agent loaded".to_string())
742    }
743
744    /// Load agent configuration from file only, **without** applying env/jenv
745    /// overrides. This is the isolation-safe counterpart of [`load`] — the
746    /// caller constructs a pristine config file and does not want ambient JACS_*
747    /// environment variables to pollute it (Issue 008).
748    pub fn load_file_only(&self, config_path: String) -> BindingResult<String> {
749        let new_agent = self.load_agent_from_config(&config_path, false, None)?;
750        *self.lock()? = new_agent;
751        Ok("Agent loaded (file-only)".to_string())
752    }
753
754    /// Load agent configuration and return canonical loaded-agent metadata.
755    pub fn load_with_info(&self, config_path: String) -> BindingResult<String> {
756        let resolved_config_path = resolve_existing_config_path(&config_path)?;
757        let password = self.configured_private_key_password()?;
758        let new_agent =
759            self.load_agent_from_config(&resolved_config_path, true, password.as_deref())?;
760        let info = jacs::simple::build_loaded_agent_info(&new_agent, &resolved_config_path)
761            .map_err(|e| BindingCoreError::agent_load(format!("Failed to load agent: {}", e)))?;
762        *self.lock()? = new_agent;
763        serialize_agent_info(&info)
764    }
765
766    /// Internal helper: load an agent from config using the new pattern.
767    ///
768    /// * `apply_env` - Whether to call `config.apply_env_overrides()` (false for file-only)
769    /// * `password` - Optional password to pass directly to Agent::from_config
770    fn load_agent_from_config(
771        &self,
772        config_path: &str,
773        apply_env: bool,
774        password: Option<&str>,
775    ) -> BindingResult<Agent> {
776        let mut config = Config::from_file(config_path)
777            .map_err(|e| BindingCoreError::agent_load(format!("Failed to load config: {}", e)))?;
778        if apply_env {
779            config.apply_env_overrides();
780        }
781        Agent::from_config(config, password)
782            .map_err(|e| BindingCoreError::agent_load(format!("Failed to load agent: {}", e)))
783    }
784
785    /// Re-root the internal file storage at `root`.
786    ///
787    /// By default `load_by_config` roots the FS backend at the current
788    /// working directory.  `verify_document_standalone` uses this to
789    /// re-root at `/` so that absolute data/key directory paths work
790    /// regardless of CWD.
791    pub fn set_storage_root(&self, root: std::path::PathBuf) -> BindingResult<()> {
792        let mut agent = self.lock()?;
793        agent
794            .set_storage_root(root)
795            .map_err(|e| BindingCoreError::generic(format!("Failed to set storage root: {}", e)))?;
796        Ok(())
797    }
798
799    /// Sign an external agent's document with this agent's registration signature.
800    pub fn sign_agent(
801        &self,
802        agent_string: &str,
803        public_key: Vec<u8>,
804        public_key_enc_type: String,
805    ) -> BindingResult<String> {
806        self.with_private_key_password(|| {
807            let mut agent = self.lock()?;
808
809            let mut external_agent: Value = agent.validate_agent(agent_string).map_err(|e| {
810                BindingCoreError::validation(format!("Agent validation failed: {}", e))
811            })?;
812
813            agent
814                .signature_verification_procedure(
815                    &external_agent,
816                    None,
817                    &AGENT_SIGNATURE_FIELDNAME.to_string(),
818                    public_key,
819                    Some(public_key_enc_type),
820                    None,
821                    None,
822                )
823                .map_err(|e| {
824                    BindingCoreError::verification_failed(format!(
825                        "Signature verification failed: {}",
826                        e
827                    ))
828                })?;
829
830            let registration_signature = agent
831                .signing_procedure(
832                    &external_agent,
833                    None,
834                    &AGENT_REGISTRATION_SIGNATURE_FIELDNAME.to_string(),
835                )
836                .map_err(|e| {
837                    BindingCoreError::signing_failed(format!("Signing procedure failed: {}", e))
838                })?;
839
840            external_agent[AGENT_REGISTRATION_SIGNATURE_FIELDNAME] = registration_signature;
841            Ok(external_agent.to_string())
842        })
843    }
844
845    /// Verify a signature on arbitrary string data.
846    pub fn verify_string(
847        &self,
848        data: &str,
849        signature_base64: &str,
850        public_key: Vec<u8>,
851        public_key_enc_type: String,
852    ) -> BindingResult<bool> {
853        let agent = self.lock()?;
854
855        if data.is_empty()
856            || signature_base64.is_empty()
857            || public_key.is_empty()
858            || public_key_enc_type.is_empty()
859        {
860            return Err(BindingCoreError::invalid_argument(format!(
861                "One parameter is empty: data={}, signature_base64={}, public_key_enc_type={}",
862                data.is_empty(),
863                signature_base64.is_empty(),
864                public_key_enc_type
865            )));
866        }
867
868        agent
869            .verify_string(
870                &data.to_string(),
871                &signature_base64.to_string(),
872                public_key,
873                Some(public_key_enc_type),
874            )
875            .map_err(|e| {
876                BindingCoreError::verification_failed(format!(
877                    "Signature verification failed: {}",
878                    e
879                ))
880            })?;
881
882        Ok(true)
883    }
884
885    /// Sign arbitrary string data with this agent's private key.
886    pub fn sign_string(&self, data: &str) -> BindingResult<String> {
887        self.with_private_key_password(|| {
888            let mut agent = self.lock()?;
889            agent.sign_string(&data.to_string()).map_err(|e| {
890                BindingCoreError::signing_failed(format!("Failed to sign string: {}", e))
891            })
892        })
893    }
894
895    /// Sign multiple messages in a single batch, decrypting the private key only once.
896    pub fn sign_batch(&self, messages: Vec<String>) -> BindingResult<Vec<String>> {
897        self.with_private_key_password(|| {
898            let mut agent = self.lock()?;
899            let refs: Vec<&str> = messages.iter().map(|s| s.as_str()).collect();
900            agent
901                .sign_batch(&refs)
902                .map_err(|e| BindingCoreError::signing_failed(format!("Batch sign failed: {}", e)))
903        })
904    }
905
906    /// Verify this agent's signature and hash.
907    pub fn verify_agent(&self, agentfile: Option<String>) -> BindingResult<bool> {
908        self.with_private_key_password(|| {
909            let mut agent = self.lock()?;
910
911            if let Some(file) = agentfile {
912                let loaded_agent = jacs::load_agent(Some(file)).map_err(|e| {
913                    BindingCoreError::agent_load(format!("Failed to load agent: {}", e))
914                })?;
915                *agent = loaded_agent;
916            }
917
918            agent.verify_self_signature().map_err(|e| {
919                BindingCoreError::verification_failed(format!(
920                    "Failed to verify agent signature: {}",
921                    e
922                ))
923            })?;
924
925            agent.verify_self_hash().map_err(|e| {
926                BindingCoreError::verification_failed(format!("Failed to verify agent hash: {}", e))
927            })?;
928
929            Ok(true)
930        })
931    }
932
933    /// Update the agent document with new data.
934    pub fn update_agent(&self, new_agent_string: &str) -> BindingResult<String> {
935        self.with_private_key_password(|| {
936            let mut agent = self.lock()?;
937            agent
938                .update_self(new_agent_string)
939                .map_err(|e| BindingCoreError::agent_load(format!("Failed to update agent: {}", e)))
940        })
941    }
942
943    /// Verify a document's signature and hash.
944    pub fn verify_document(&self, document_string: &str) -> BindingResult<bool> {
945        let mut agent = self.lock()?;
946
947        let doc = agent.load_document(document_string).map_err(|e| {
948            BindingCoreError::document_failed(format!("Failed to load document: {}", e))
949        })?;
950
951        let document_key = doc.getkey();
952        let value = doc.getvalue();
953
954        agent.verify_hash(value).map_err(|e| {
955            BindingCoreError::verification_failed(format!("Failed to verify document hash: {}", e))
956        })?;
957
958        // Prefer the currently loaded agent's public key first. This keeps
959        // local self-verification fast and avoids falling through to remote key
960        // resolution for documents we just signed in the same workspace.
961        if agent
962            .verify_document_signature(&document_key, None, None, None, None)
963            .is_err()
964        {
965            agent
966                .verify_external_document_signature(&document_key)
967                .map_err(|e| {
968                    BindingCoreError::verification_failed(format!(
969                        "Failed to verify document signature: {}",
970                        e
971                    ))
972                })?;
973        }
974
975        Ok(true)
976    }
977
978    /// Update an existing document.
979    pub fn update_document(
980        &self,
981        document_key: &str,
982        new_document_string: &str,
983        attachments: Option<Vec<String>>,
984        embed: Option<bool>,
985    ) -> BindingResult<String> {
986        self.with_private_key_password(|| {
987            let mut agent = self.lock()?;
988
989            let doc = agent
990                .update_document(document_key, new_document_string, attachments, embed)
991                .map_err(|e| {
992                    BindingCoreError::document_failed(format!("Failed to update document: {}", e))
993                })?;
994
995            Ok(doc.to_string())
996        })
997    }
998
999    /// Verify a document's signature with an optional custom signature field.
1000    pub fn verify_signature(
1001        &self,
1002        document_string: &str,
1003        signature_field: Option<String>,
1004    ) -> BindingResult<bool> {
1005        let mut agent = self.lock()?;
1006
1007        let doc = agent.load_document(document_string).map_err(|e| {
1008            BindingCoreError::document_failed(format!("Failed to load document: {}", e))
1009        })?;
1010
1011        let document_key = doc.getkey();
1012        let sig_field_ref = signature_field.as_ref();
1013
1014        agent
1015            .verify_document_signature(
1016                &document_key,
1017                sig_field_ref.map(|s| s.as_str()),
1018                None,
1019                None,
1020                None,
1021            )
1022            .map_err(|e| {
1023                BindingCoreError::verification_failed(format!("Failed to verify signature: {}", e))
1024            })?;
1025
1026        Ok(true)
1027    }
1028
1029    /// Create an agreement on a document.
1030    pub fn create_agreement(
1031        &self,
1032        document_string: &str,
1033        agentids: Vec<String>,
1034        question: Option<String>,
1035        context: Option<String>,
1036        agreement_fieldname: Option<String>,
1037    ) -> BindingResult<String> {
1038        self.create_agreement_with_options(
1039            document_string,
1040            agentids,
1041            question,
1042            context,
1043            agreement_fieldname,
1044            None,
1045            None,
1046            None,
1047            None,
1048        )
1049    }
1050
1051    /// Create an agreement with extended options (timeout, quorum, algorithm constraints).
1052    ///
1053    /// All option parameters are optional:
1054    /// - `timeout`: ISO 8601 deadline after which the agreement expires
1055    /// - `quorum`: minimum number of signatures required (M-of-N)
1056    /// - `required_algorithms`: only accept signatures from these algorithms
1057    /// - `minimum_strength`: "classical" or "post-quantum"
1058    pub fn create_agreement_with_options(
1059        &self,
1060        document_string: &str,
1061        agentids: Vec<String>,
1062        question: Option<String>,
1063        context: Option<String>,
1064        agreement_fieldname: Option<String>,
1065        timeout: Option<String>,
1066        quorum: Option<u32>,
1067        required_algorithms: Option<Vec<String>>,
1068        minimum_strength: Option<String>,
1069    ) -> BindingResult<String> {
1070        use jacs::agent::agreement::{Agreement, AgreementOptions};
1071
1072        self.with_private_key_password(|| {
1073            let mut agent = self.lock()?;
1074            let base_doc = ensure_editable_agreement_document(&mut agent, document_string)?;
1075            let document_key = base_doc.getkey();
1076
1077            let options = AgreementOptions {
1078                timeout,
1079                quorum,
1080                required_algorithms,
1081                minimum_strength,
1082            };
1083
1084            let agreement_doc = agent
1085                .create_agreement_with_options(
1086                    &document_key,
1087                    agentids.as_slice(),
1088                    question.as_deref(),
1089                    context.as_deref(),
1090                    agreement_fieldname,
1091                    &options,
1092                )
1093                .map_err(|e| {
1094                    BindingCoreError::agreement_failed(format!("Failed to create agreement: {}", e))
1095                })?;
1096
1097            Ok(agreement_doc.value.to_string())
1098        })
1099    }
1100
1101    /// Sign an agreement on a document.
1102    pub fn sign_agreement(
1103        &self,
1104        document_string: &str,
1105        agreement_fieldname: Option<String>,
1106    ) -> BindingResult<String> {
1107        self.with_private_key_password(|| {
1108            let mut agent = self.lock()?;
1109            let doc = agent.load_document(document_string).map_err(|e| {
1110                BindingCoreError::document_failed(format!("Failed to load document: {}", e))
1111            })?;
1112            let document_key = doc.getkey();
1113            let signed_doc = agent
1114                .sign_agreement(&document_key, agreement_fieldname)
1115                .map_err(|e| {
1116                    BindingCoreError::agreement_failed(format!("Failed to sign agreement: {}", e))
1117                })?;
1118
1119            Ok(signed_doc.value.to_string())
1120        })
1121    }
1122
1123    /// Create a new JACS document.
1124    pub fn create_document(
1125        &self,
1126        document_string: &str,
1127        custom_schema: Option<String>,
1128        outputfilename: Option<String>,
1129        no_save: bool,
1130        attachments: Option<&str>,
1131        embed: Option<bool>,
1132    ) -> BindingResult<String> {
1133        self.with_private_key_password(|| {
1134            let mut agent = self.lock()?;
1135
1136            jacs::shared::document_create(
1137                &mut agent,
1138                document_string,
1139                custom_schema,
1140                outputfilename,
1141                no_save,
1142                attachments,
1143                embed,
1144            )
1145            .map_err(|e| {
1146                BindingCoreError::document_failed(format!("Failed to create document: {}", e))
1147            })
1148        })
1149    }
1150
1151    /// Persist an already-signed JACS document and return its lookup key.
1152    ///
1153    /// Stores the document both in the agent's data directory (for file-based
1154    /// access) and in the storage index (`documents/`) so that
1155    /// `list_document_keys()` can find it.
1156    pub fn save_signed_document(
1157        &self,
1158        document_string: &str,
1159        outputfilename: Option<String>,
1160        export_embedded: Option<bool>,
1161        extract_only: Option<bool>,
1162    ) -> BindingResult<String> {
1163        let mut agent = self.lock()?;
1164        let doc = agent.load_document(document_string).map_err(|e| {
1165            BindingCoreError::document_failed(format!("Failed to load signed document: {}", e))
1166        })?;
1167        let document_key = doc.getkey();
1168        agent
1169            .save_document(&document_key, outputfilename, export_embedded, extract_only)
1170            .map_err(|e| {
1171                BindingCoreError::document_failed(format!(
1172                    "Failed to persist signed document '{}': {}",
1173                    document_key, e
1174                ))
1175            })?;
1176
1177        Ok(document_key)
1178    }
1179
1180    /// Return all known document lookup keys from the agent's configured storage.
1181    pub fn list_document_keys(&self) -> BindingResult<Vec<String>> {
1182        let mut agent = self.lock()?;
1183        Ok(agent.get_document_keys())
1184    }
1185
1186    /// Check an agreement on a document.
1187    pub fn check_agreement(
1188        &self,
1189        document_string: &str,
1190        agreement_fieldname: Option<String>,
1191    ) -> BindingResult<String> {
1192        let mut agent = self.lock()?;
1193        let doc = agent.load_document(document_string).map_err(|e| {
1194            BindingCoreError::document_failed(format!("Failed to load document: {}", e))
1195        })?;
1196        let document_key = doc.getkey();
1197        let agreement_fieldname_key = agreement_fieldname
1198            .clone()
1199            .unwrap_or_else(|| AGENT_AGREEMENT_FIELDNAME.to_string());
1200
1201        agent
1202            .check_agreement(&document_key, Some(agreement_fieldname_key.clone()))
1203            .map_err(|e| {
1204                BindingCoreError::agreement_failed(format!("Failed to check agreement: {}", e))
1205            })?;
1206
1207        let requested = doc
1208            .agreement_requested_agents(Some(agreement_fieldname_key.clone()))
1209            .map_err(|e| {
1210                BindingCoreError::agreement_failed(format!(
1211                    "Failed to read requested signers: {}",
1212                    e
1213                ))
1214            })?;
1215
1216        let pending = doc
1217            .agreement_unsigned_agents(Some(agreement_fieldname_key.clone()))
1218            .map_err(|e| {
1219                BindingCoreError::agreement_failed(format!("Failed to read pending signers: {}", e))
1220            })?;
1221
1222        let signatures = doc
1223            .value
1224            .get(&agreement_fieldname_key)
1225            .and_then(|agreement| agreement.get("signatures"))
1226            .and_then(|v| v.as_array())
1227            .cloned()
1228            .unwrap_or_default();
1229
1230        let mut signed_at_by_agent: HashMap<String, String> = HashMap::new();
1231        for signature in signatures {
1232            if let Some(agent_id) = signature.get("agentID").and_then(|v| v.as_str()) {
1233                let normalized = normalize_agent_id_for_compare(agent_id).to_string();
1234                let signed_at = signature
1235                    .get("date")
1236                    .and_then(|v| v.as_str())
1237                    .unwrap_or("")
1238                    .to_string();
1239                signed_at_by_agent.insert(normalized, signed_at);
1240            }
1241        }
1242
1243        let signers = requested
1244            .iter()
1245            .map(|agent_id| {
1246                let normalized = normalize_agent_id_for_compare(agent_id).to_string();
1247                let signed_at = signed_at_by_agent
1248                    .get(&normalized)
1249                    .filter(|ts| !ts.is_empty())
1250                    .cloned();
1251                let signed = signed_at.is_some();
1252                let mut signer = json!({
1253                    "agentId": agent_id,
1254                    "agent_id": agent_id,
1255                    "signed": signed
1256                });
1257                if let Some(ts) = signed_at {
1258                    signer["signedAt"] = json!(ts.clone());
1259                    signer["signed_at"] = json!(ts);
1260                }
1261                signer
1262            })
1263            .collect::<Vec<Value>>();
1264
1265        let result = json!({
1266            "complete": pending.is_empty(),
1267            "signers": signers,
1268            "pending": pending
1269        });
1270
1271        Ok(result.to_string())
1272    }
1273
1274    /// Sign a request payload (wraps in a JACS document).
1275    pub fn sign_request(&self, payload_value: Value) -> BindingResult<String> {
1276        self.with_private_key_password(|| {
1277            let mut agent = self.lock()?;
1278
1279            let wrapper_value = serde_json::json!({
1280                "jacs_payload": payload_value
1281            });
1282
1283            let wrapper_string = serde_json::to_string(&wrapper_value).map_err(|e| {
1284                BindingCoreError::serialization_failed(format!(
1285                    "Failed to serialize wrapper JSON: {}",
1286                    e
1287                ))
1288            })?;
1289
1290            jacs::shared::document_create(
1291                &mut agent,
1292                &wrapper_string,
1293                None,
1294                None,
1295                true, // no_save
1296                None,
1297                Some(false),
1298            )
1299            .map_err(|e| {
1300                BindingCoreError::document_failed(format!("Failed to create document: {}", e))
1301            })
1302        })
1303    }
1304
1305    /// Verify a response payload and return the payload value.
1306    pub fn verify_response(&self, document_string: String) -> BindingResult<Value> {
1307        let mut agent = self.lock()?;
1308
1309        agent
1310            .verify_payload(document_string, None)
1311            .map_err(|e| BindingCoreError::verification_failed(e.to_string()))
1312    }
1313
1314    /// Verify a response payload and return (payload, agent_id).
1315    pub fn verify_response_with_agent_id(
1316        &self,
1317        document_string: String,
1318    ) -> BindingResult<(Value, String)> {
1319        let mut agent = self.lock()?;
1320
1321        agent
1322            .verify_payload_with_agent_id(document_string, None)
1323            .map_err(|e| BindingCoreError::verification_failed(e.to_string()))
1324    }
1325
1326    /// Verify a document looked up by its ID from storage.
1327    ///
1328    /// This is a convenience method for when you have a document ID rather than
1329    /// the full JSON string. The document ID should be in "uuid:version" format.
1330    pub fn verify_document_by_id(&self, document_id: &str) -> BindingResult<bool> {
1331        use jacs::storage::StorageDocumentTraits;
1332
1333        // Validate format
1334        if !document_id.contains(':') {
1335            return Err(BindingCoreError::invalid_argument(format!(
1336                "Document ID must be in 'uuid:version' format, got '{}'. \
1337                Use verify_document() with the full JSON string instead.",
1338                document_id
1339            )));
1340        }
1341
1342        let storage = jacs::storage::MultiStorage::default_new().map_err(|e| {
1343            BindingCoreError::generic(format!("Failed to initialize storage: {}", e))
1344        })?;
1345
1346        let doc = storage.get_document(document_id).map_err(|e| {
1347            BindingCoreError::document_failed(format!(
1348                "Failed to load document '{}' from storage: {}",
1349                document_id, e
1350            ))
1351        })?;
1352
1353        let doc_str = serde_json::to_string(&doc.value).map_err(|e| {
1354            BindingCoreError::serialization_failed(format!(
1355                "Failed to serialize document '{}': {}",
1356                document_id, e
1357            ))
1358        })?;
1359
1360        self.verify_document(&doc_str)
1361    }
1362
1363    /// Load a document by ID from the agent's configured storage.
1364    ///
1365    /// The document ID should be in "uuid:version" format.
1366    pub fn get_document_by_id(&self, document_id: &str) -> BindingResult<String> {
1367        if !document_id.contains(':') {
1368            return Err(BindingCoreError::invalid_argument(format!(
1369                "Document ID must be in 'uuid:version' format, got '{}'.",
1370                document_id
1371            )));
1372        }
1373
1374        let agent = self.lock()?;
1375        let doc = agent.get_document(document_id).map_err(|e| {
1376            BindingCoreError::document_failed(format!(
1377                "Failed to load document '{}' from storage: {}",
1378                document_id, e
1379            ))
1380        })?;
1381
1382        serde_json::to_string(&doc.value).map_err(|e| {
1383            BindingCoreError::serialization_failed(format!(
1384                "Failed to serialize document '{}': {}",
1385                document_id, e
1386            ))
1387        })
1388    }
1389
1390    /// Get the loaded agent's canonical JACS identifier.
1391    pub fn get_agent_id(&self) -> BindingResult<String> {
1392        let agent = self.lock()?;
1393        let value = agent
1394            .get_value()
1395            .ok_or_else(|| BindingCoreError::agent_load("Agent not loaded. Call load() first."))?;
1396        value
1397            .get("jacsId")
1398            .and_then(|v| v.as_str())
1399            .map(str::to_string)
1400            .filter(|id| !id.is_empty())
1401            .ok_or_else(|| {
1402                BindingCoreError::agent_load(
1403                    "Agent not loaded or has no jacsId. Call load() first.",
1404                )
1405            })
1406    }
1407
1408    /// Re-encrypt the agent's private key with a new password.
1409    ///
1410    /// Reads the encrypted private key file, decrypts with old_password,
1411    /// validates new_password, re-encrypts, and writes the updated file.
1412    pub fn reencrypt_key(&self, old_password: &str, new_password: &str) -> BindingResult<()> {
1413        use jacs::crypt::aes_encrypt::reencrypt_private_key;
1414
1415        // Find key path from config
1416        let agent = self.lock()?;
1417        let key_path = if let Some(config) = &agent.config {
1418            let key_dir = config
1419                .jacs_key_directory()
1420                .as_deref()
1421                .unwrap_or("./jacs_keys");
1422            let key_file = config
1423                .jacs_agent_private_key_filename()
1424                .as_deref()
1425                .unwrap_or("jacs.private.pem.enc");
1426            format!("{}/{}", key_dir, key_file)
1427        } else {
1428            "./jacs_keys/jacs.private.pem.enc".to_string()
1429        };
1430        drop(agent);
1431
1432        let encrypted_data = std::fs::read(&key_path).map_err(|e| {
1433            BindingCoreError::generic(format!(
1434                "Failed to read private key file '{}': {}",
1435                key_path, e
1436            ))
1437        })?;
1438
1439        let re_encrypted = reencrypt_private_key(&encrypted_data, old_password, new_password)
1440            .map_err(|e| BindingCoreError::generic(format!("Re-encryption failed: {}", e)))?;
1441
1442        std::fs::write(&key_path, &re_encrypted).map_err(|e| {
1443            BindingCoreError::generic(format!(
1444                "Failed to write re-encrypted key to '{}': {}",
1445                key_path, e
1446            ))
1447        })?;
1448
1449        Ok(())
1450    }
1451
1452    /// Create an ephemeral in-memory agent. No config, no files, no env vars needed.
1453    ///
1454    /// Replaces the inner agent with a freshly created ephemeral agent that
1455    /// lives entirely in memory. Returns a JSON string with agent info
1456    /// (agent_id, name, version, algorithm). Default algorithm is `pq2025`.
1457    pub fn ephemeral(&self, algorithm: Option<&str>) -> BindingResult<String> {
1458        // Map user-friendly names to internal algorithm strings
1459        let algo = match algorithm.unwrap_or("pq2025") {
1460            "ed25519" => "ring-Ed25519",
1461            "rsa-pss" => "RSA-PSS",
1462            "pq2025" => "pq2025",
1463            other => other,
1464        };
1465
1466        let mut agent = Agent::ephemeral(algo).map_err(|e| {
1467            BindingCoreError::agent_load(format!("Failed to create ephemeral agent: {}", e))
1468        })?;
1469
1470        let template = jacs::create_minimal_blank_agent("ai".to_string(), None, None, None)
1471            .map_err(|e| {
1472                BindingCoreError::agent_load(format!(
1473                    "Failed to create minimal agent template: {}",
1474                    e
1475                ))
1476            })?;
1477        let mut agent_json: Value = serde_json::from_str(&template).map_err(|e| {
1478            BindingCoreError::serialization_failed(format!(
1479                "Failed to parse agent template JSON: {}",
1480                e
1481            ))
1482        })?;
1483        if let Some(obj) = agent_json.as_object_mut() {
1484            obj.insert("name".to_string(), json!("ephemeral"));
1485            obj.insert("description".to_string(), json!("Ephemeral JACS agent"));
1486        }
1487
1488        let instance = agent
1489            .create_agent_and_load(&agent_json.to_string(), true, Some(algo))
1490            .map_err(|e| {
1491                BindingCoreError::agent_load(format!("Failed to initialize ephemeral agent: {}", e))
1492            })?;
1493
1494        let agent_id = instance["jacsId"].as_str().unwrap_or("").to_string();
1495        let version = instance["jacsVersion"].as_str().unwrap_or("").to_string();
1496
1497        // Replace the inner agent with the ephemeral one
1498        let mut inner = self.lock()?;
1499        *inner = agent;
1500
1501        let info = json!({
1502            "agent_id": agent_id,
1503            "name": "ephemeral",
1504            "version": version,
1505            "algorithm": algo,
1506        });
1507
1508        serde_json::to_string_pretty(&info).map_err(|e| {
1509            BindingCoreError::serialization_failed(format!(
1510                "Failed to serialize ephemeral agent info: {}",
1511                e
1512            ))
1513        })
1514    }
1515
1516    /// Returns diagnostic information including loaded agent details as a JSON string.
1517    pub fn diagnostics(&self) -> String {
1518        let mut info = jacs::simple::diagnostics();
1519
1520        if let Ok(agent) = self.inner.lock() {
1521            if agent.ready() {
1522                info["agent_loaded"] = json!(true);
1523                if let Some(value) = agent.get_value() {
1524                    info["agent_id"] = json!(value.get("jacsId").and_then(|v| v.as_str()));
1525                    info["agent_version"] =
1526                        json!(value.get("jacsVersion").and_then(|v| v.as_str()));
1527                }
1528            }
1529            if let Some(config) = &agent.config {
1530                if let Some(dir) = config.jacs_data_directory().as_ref() {
1531                    info["data_directory"] = json!(dir);
1532                }
1533                if let Some(dir) = config.jacs_key_directory().as_ref() {
1534                    info["key_directory"] = json!(dir);
1535                }
1536                if let Some(storage) = config.jacs_default_storage().as_ref() {
1537                    info["default_storage"] = json!(storage);
1538                }
1539                if let Some(algo) = config.jacs_agent_key_algorithm().as_ref() {
1540                    info["key_algorithm"] = json!(algo);
1541                }
1542            }
1543        }
1544
1545        serde_json::to_string_pretty(&info).unwrap_or_default()
1546    }
1547
1548    /// Returns setup instructions for publishing DNS records and enabling DNSSEC.
1549    ///
1550    /// Requires a loaded agent (call `load()` first).
1551    pub fn get_setup_instructions(&self, domain: &str, ttl: u32) -> BindingResult<String> {
1552        use jacs::agent::boilerplate::BoilerPlate;
1553        use jacs::dns::bootstrap::{
1554            DigestEncoding, build_dns_record, dnssec_guidance, emit_azure_cli,
1555            emit_cloudflare_curl, emit_gcloud_dns, emit_plain_bind, emit_route53_change_batch,
1556            tld_requirement_text,
1557        };
1558
1559        let agent = self.lock()?;
1560        let agent_value = agent.get_value().cloned().unwrap_or(json!({}));
1561        let agent_id = agent_value
1562            .get("jacsId")
1563            .and_then(|v| v.as_str())
1564            .unwrap_or("");
1565        if agent_id.is_empty() {
1566            return Err(BindingCoreError::agent_load(
1567                "Agent not loaded or has no jacsId. Call load() first.",
1568            ));
1569        }
1570
1571        let pk = agent
1572            .get_public_key()
1573            .map_err(|e| BindingCoreError::generic(format!("Failed to get public key: {}", e)))?;
1574        let digest = jacs::dns::bootstrap::pubkey_digest_b64(&pk);
1575        let rr = build_dns_record(domain, ttl, agent_id, &digest, DigestEncoding::Base64);
1576
1577        let dns_record_bind = emit_plain_bind(&rr);
1578        let dns_owner = rr.owner.clone();
1579        let dns_record_value = rr.txt.clone();
1580
1581        let mut provider_commands = std::collections::HashMap::new();
1582        provider_commands.insert("bind".to_string(), dns_record_bind.clone());
1583        provider_commands.insert("route53".to_string(), emit_route53_change_batch(&rr));
1584        provider_commands.insert("gcloud".to_string(), emit_gcloud_dns(&rr, "YOUR_ZONE_NAME"));
1585        provider_commands.insert(
1586            "azure".to_string(),
1587            emit_azure_cli(&rr, "YOUR_RG", domain, "_v1.agent.jacs"),
1588        );
1589        provider_commands.insert(
1590            "cloudflare".to_string(),
1591            emit_cloudflare_curl(&rr, "YOUR_ZONE_ID"),
1592        );
1593
1594        let mut dnssec_instructions = std::collections::HashMap::new();
1595        for name in &["aws", "cloudflare", "azure", "gcloud"] {
1596            dnssec_instructions.insert(name.to_string(), dnssec_guidance(name).to_string());
1597        }
1598
1599        let tld_requirement = tld_requirement_text().to_string();
1600
1601        let well_known = json!({
1602            "jacs_agent_id": agent_id,
1603            "jacs_public_key_hash": digest,
1604            "jacs_dns_record": dns_owner,
1605        });
1606        let well_known_json = serde_json::to_string_pretty(&well_known).unwrap_or_default();
1607
1608        let summary = format!(
1609            "Setup instructions for agent {agent_id} on domain {domain}:\n\
1610             \n\
1611             1. DNS: Publish the following TXT record:\n\
1612             {bind}\n\
1613             \n\
1614             2. DNSSEC: {dnssec}\n\
1615             \n\
1616             3. Domain requirement: {tld}\n\
1617             \n\
1618             4. .well-known: Serve the well-known JSON at /.well-known/jacs-agent.json",
1619            agent_id = agent_id,
1620            domain = domain,
1621            bind = dns_record_bind,
1622            dnssec = dnssec_guidance("aws"),
1623            tld = tld_requirement,
1624        );
1625
1626        let result = json!({
1627            "dns_record_bind": dns_record_bind,
1628            "dns_record_value": dns_record_value,
1629            "dns_owner": dns_owner,
1630            "provider_commands": provider_commands,
1631            "dnssec_instructions": dnssec_instructions,
1632            "tld_requirement": tld_requirement,
1633            "well_known_json": well_known_json,
1634            "summary": summary,
1635        });
1636
1637        serde_json::to_string_pretty(&result).map_err(|e| {
1638            BindingCoreError::serialization_failed(format!(
1639                "Failed to serialize setup instructions: {}",
1640                e
1641            ))
1642        })
1643    }
1644
1645    /// Export the loaded agent's full JSON document.
1646    pub fn export_agent(&self) -> BindingResult<String> {
1647        let agent = self.lock()?;
1648        let value = agent
1649            .get_value()
1650            .cloned()
1651            .ok_or_else(|| BindingCoreError::agent_load("Agent not loaded. Call load() first."))?;
1652        serde_json::to_string_pretty(&value).map_err(|e| {
1653            BindingCoreError::serialization_failed(format!(
1654                "Failed to serialize agent document: {}",
1655                e
1656            ))
1657        })
1658    }
1659
1660    /// Get the loaded agent's public key as a PEM string.
1661    pub fn get_public_key_pem(&self) -> BindingResult<String> {
1662        let agent = self.lock()?;
1663        let public_key = BoilerPlate::get_public_key(&*agent)
1664            .map_err(|e| BindingCoreError::generic(format!("Failed to get public key: {}", e)))?;
1665        Ok(jacs::crypt::normalize_public_key_pem(&public_key))
1666    }
1667
1668    /// Get the agent's JSON representation as a string.
1669    ///
1670    /// Returns the agent's full JSON document.
1671    pub fn get_agent_json(&self) -> BindingResult<String> {
1672        self.export_agent()
1673    }
1674}
1675
1676#[cfg(feature = "a2a")]
1677impl AgentWrapper {
1678    // =========================================================================
1679    // A2A Protocol Methods
1680    // =========================================================================
1681
1682    /// Export this agent as an A2A Agent Card (v0.4.0).
1683    ///
1684    /// Returns the Agent Card as a JSON string.
1685    pub fn export_agent_card(&self) -> BindingResult<String> {
1686        let agent = self.lock()?;
1687        let card = jacs::a2a::agent_card::export_agent_card(&agent).map_err(|e| {
1688            BindingCoreError::generic(format!("Failed to export agent card: {}", e))
1689        })?;
1690        serde_json::to_string_pretty(&card).map_err(|e| {
1691            BindingCoreError::serialization_failed(format!("Failed to serialize agent card: {}", e))
1692        })
1693    }
1694
1695    /// Generate all .well-known documents for A2A discovery.
1696    ///
1697    /// Returns a JSON string containing an array of [path, document] pairs.
1698    pub fn generate_well_known_documents(
1699        &self,
1700        a2a_algorithm: Option<&str>,
1701    ) -> BindingResult<String> {
1702        let agent = self.lock()?;
1703        let card = jacs::a2a::agent_card::export_agent_card(&agent).map_err(|e| {
1704            BindingCoreError::generic(format!("Failed to export agent card: {}", e))
1705        })?;
1706
1707        let a2a_alg = a2a_algorithm.unwrap_or("ring-Ed25519");
1708        let dual_keys = jacs::a2a::keys::create_jwk_keys(None, Some(a2a_alg)).map_err(|e| {
1709            BindingCoreError::generic(format!("Failed to generate A2A keys: {}", e))
1710        })?;
1711
1712        let agent_id = agent
1713            .get_id()
1714            .map_err(|e| BindingCoreError::generic(format!("Failed to get agent ID: {}", e)))?;
1715
1716        let jws = jacs::a2a::extension::sign_agent_card_jws(
1717            &card,
1718            &dual_keys.a2a_private_key,
1719            &dual_keys.a2a_algorithm,
1720            &agent_id,
1721        )
1722        .map_err(|e| BindingCoreError::generic(format!("Failed to sign Agent Card: {}", e)))?;
1723
1724        let documents = jacs::a2a::extension::generate_well_known_documents(
1725            &agent,
1726            &card,
1727            &dual_keys.a2a_public_key,
1728            &dual_keys.a2a_algorithm,
1729            &jws,
1730        )
1731        .map_err(|e| {
1732            BindingCoreError::generic(format!("Failed to generate well-known documents: {}", e))
1733        })?;
1734
1735        // Serialize as JSON array of [path, document] pairs
1736        let pairs: Vec<Value> = documents
1737            .into_iter()
1738            .map(|(path, doc)| serde_json::json!({ "path": path, "document": doc }))
1739            .collect();
1740        serde_json::to_string_pretty(&pairs).map_err(|e| {
1741            BindingCoreError::serialization_failed(format!(
1742                "Failed to serialize well-known documents: {}",
1743                e
1744            ))
1745        })
1746    }
1747
1748    /// Wrap an A2A artifact with JACS provenance signature.
1749    ///
1750    /// Returns the signed wrapped artifact as a JSON string.
1751    #[deprecated(since = "0.9.0", note = "Use sign_artifact() instead")]
1752    pub fn wrap_a2a_artifact(
1753        &self,
1754        artifact_json: &str,
1755        artifact_type: &str,
1756        parent_signatures_json: Option<&str>,
1757    ) -> BindingResult<String> {
1758        if std::env::var("JACS_SHOW_DEPRECATIONS").is_ok() {
1759            tracing::warn!("wrap_a2a_artifact is deprecated, use sign_artifact instead");
1760        }
1761
1762        let artifact: Value = serde_json::from_str(artifact_json).map_err(|e| {
1763            BindingCoreError::invalid_argument(format!("Invalid artifact JSON: {}", e))
1764        })?;
1765
1766        let parent_signatures: Option<Vec<Value>> = match parent_signatures_json {
1767            Some(json_str) => {
1768                let parsed: Vec<Value> = serde_json::from_str(json_str).map_err(|e| {
1769                    BindingCoreError::invalid_argument(format!(
1770                        "Invalid parent signatures JSON array: {}",
1771                        e
1772                    ))
1773                })?;
1774                Some(parsed)
1775            }
1776            None => None,
1777        };
1778
1779        let mut agent = self.lock()?;
1780        let wrapped = jacs::a2a::provenance::wrap_artifact_with_provenance(
1781            &mut agent,
1782            artifact,
1783            artifact_type,
1784            parent_signatures,
1785        )
1786        .map_err(|e| BindingCoreError::signing_failed(format!("Failed to wrap artifact: {}", e)))?;
1787
1788        serde_json::to_string_pretty(&wrapped).map_err(|e| {
1789            BindingCoreError::serialization_failed(format!(
1790                "Failed to serialize wrapped artifact: {}",
1791                e
1792            ))
1793        })
1794    }
1795
1796    /// Sign an A2A artifact with JACS provenance.
1797    ///
1798    /// This is the recommended primary API, replacing the deprecated
1799    /// [`wrap_a2a_artifact`](Self::wrap_a2a_artifact).
1800    pub fn sign_artifact(
1801        &self,
1802        artifact_json: &str,
1803        artifact_type: &str,
1804        parent_signatures_json: Option<&str>,
1805    ) -> BindingResult<String> {
1806        #[allow(deprecated)]
1807        self.wrap_a2a_artifact(artifact_json, artifact_type, parent_signatures_json)
1808    }
1809
1810    /// Verify a JACS-wrapped A2A artifact.
1811    ///
1812    /// Returns the verification result as a JSON string.
1813    pub fn verify_a2a_artifact(&self, wrapped_json: &str) -> BindingResult<String> {
1814        let wrapped: Value = serde_json::from_str(wrapped_json).map_err(|e| {
1815            BindingCoreError::invalid_argument(format!("Invalid wrapped artifact JSON: {}", e))
1816        })?;
1817
1818        let agent = self.lock()?;
1819        let result =
1820            jacs::a2a::provenance::verify_wrapped_artifact(&agent, &wrapped).map_err(|e| {
1821                BindingCoreError::verification_failed(format!(
1822                    "A2A artifact verification error: {}",
1823                    e
1824                ))
1825            })?;
1826
1827        serde_json::to_string_pretty(&result).map_err(|e| {
1828            BindingCoreError::serialization_failed(format!(
1829                "Failed to serialize verification result: {}",
1830                e
1831            ))
1832        })
1833    }
1834    /// Assess trust level of a remote A2A agent given its Agent Card JSON.
1835    ///
1836    /// Returns the trust assessment as a JSON string.
1837    pub fn assess_a2a_agent(&self, agent_card_json: &str, policy: &str) -> BindingResult<String> {
1838        use jacs::a2a::AgentCard;
1839        use jacs::a2a::trust::{A2ATrustPolicy, assess_a2a_agent};
1840
1841        let card: AgentCard = serde_json::from_str(agent_card_json).map_err(|e| {
1842            BindingCoreError::invalid_argument(format!("Invalid Agent Card JSON: {}", e))
1843        })?;
1844
1845        let trust_policy = A2ATrustPolicy::from_str_loose(policy).map_err(|e| {
1846            BindingCoreError::invalid_argument(format!("Invalid trust policy '{}': {}", policy, e))
1847        })?;
1848
1849        let agent = self.lock()?;
1850        let assessment = assess_a2a_agent(&agent, &card, trust_policy);
1851
1852        serde_json::to_string_pretty(&assessment).map_err(|e| {
1853            BindingCoreError::serialization_failed(format!(
1854                "Failed to serialize trust assessment: {}",
1855                e
1856            ))
1857        })
1858    }
1859
1860    /// Verify a JACS-wrapped A2A artifact with trust policy enforcement.
1861    ///
1862    /// Combines cryptographic signature verification with trust policy evaluation.
1863    /// The remote agent's Agent Card is assessed against the specified policy,
1864    /// and the trust level is included in the verification result.
1865    ///
1866    /// # Arguments
1867    ///
1868    /// * `wrapped_json` - JSON string of the JACS-wrapped artifact
1869    /// * `agent_card_json` - JSON string of the remote agent's A2A Agent Card
1870    /// * `policy` - Trust policy name: "open", "verified", or "strict"
1871    ///
1872    /// # Returns
1873    ///
1874    /// JSON string containing the verification result with trust information.
1875    pub fn verify_a2a_artifact_with_policy(
1876        &self,
1877        wrapped_json: &str,
1878        agent_card_json: &str,
1879        policy: &str,
1880    ) -> BindingResult<String> {
1881        use jacs::a2a::AgentCard;
1882        use jacs::a2a::trust::A2ATrustPolicy;
1883
1884        let wrapped: Value = serde_json::from_str(wrapped_json).map_err(|e| {
1885            BindingCoreError::invalid_argument(format!("Invalid wrapped artifact JSON: {}", e))
1886        })?;
1887
1888        let card: AgentCard = serde_json::from_str(agent_card_json).map_err(|e| {
1889            BindingCoreError::invalid_argument(format!("Invalid Agent Card JSON: {}", e))
1890        })?;
1891
1892        let trust_policy = A2ATrustPolicy::from_str_loose(policy).map_err(|e| {
1893            BindingCoreError::invalid_argument(format!("Invalid trust policy '{}': {}", policy, e))
1894        })?;
1895
1896        let agent = self.lock()?;
1897        let result = jacs::a2a::provenance::verify_wrapped_artifact_with_policy(
1898            &agent,
1899            &wrapped,
1900            &card,
1901            trust_policy,
1902        )
1903        .map_err(|e| {
1904            BindingCoreError::verification_failed(format!(
1905                "A2A artifact verification with policy error: {}",
1906                e
1907            ))
1908        })?;
1909
1910        serde_json::to_string_pretty(&result).map_err(|e| {
1911            BindingCoreError::serialization_failed(format!(
1912                "Failed to serialize verification result: {}",
1913                e
1914            ))
1915        })
1916    }
1917}
1918
1919impl AgentWrapper {
1920    // =========================================================================
1921    // Attestation API (gated behind `attestation` feature)
1922    // =========================================================================
1923
1924    /// Create a signed attestation document from JSON parameters.
1925    ///
1926    /// The `params_json` string must be a JSON object with:
1927    /// - `subject` (required): `{ type, id, digests: { sha256, ... } }`
1928    /// - `claims` (required): `[{ name, value, confidence?, assuranceLevel?, ... }]`
1929    /// - `evidence` (optional): array of evidence references
1930    /// - `derivation` (optional): derivation/transform receipt
1931    /// - `policyContext` (optional): policy evaluation context
1932    ///
1933    /// Returns the signed attestation document as a JSON string.
1934    #[cfg(feature = "attestation")]
1935    pub fn create_attestation(&self, params_json: &str) -> BindingResult<String> {
1936        use jacs::attestation::AttestationTraits;
1937        use jacs::attestation::types::*;
1938
1939        let params: Value = serde_json::from_str(params_json).map_err(|e| {
1940            BindingCoreError::serialization_failed(format!(
1941                "Failed to parse attestation params JSON: {}. \
1942                 Provide a valid JSON object with 'subject' and 'claims' fields.",
1943                e
1944            ))
1945        })?;
1946
1947        // Parse subject (required)
1948        let subject: AttestationSubject =
1949            serde_json::from_value(params.get("subject").cloned().ok_or_else(|| {
1950                BindingCoreError::validation(
1951                    "Missing required 'subject' field in attestation params",
1952                )
1953            })?)
1954            .map_err(|e| BindingCoreError::validation(format!("Invalid 'subject' field: {}", e)))?;
1955
1956        // Parse claims (required, at least 1)
1957        let claims: Vec<Claim> =
1958            serde_json::from_value(params.get("claims").cloned().ok_or_else(|| {
1959                BindingCoreError::validation(
1960                    "Missing required 'claims' field in attestation params",
1961                )
1962            })?)
1963            .map_err(|e| BindingCoreError::validation(format!("Invalid 'claims' field: {}", e)))?;
1964
1965        // Parse optional evidence
1966        let evidence: Vec<EvidenceRef> = if let Some(ev) = params.get("evidence") {
1967            serde_json::from_value(ev.clone()).map_err(|e| {
1968                BindingCoreError::validation(format!("Invalid 'evidence' field: {}", e))
1969            })?
1970        } else {
1971            vec![]
1972        };
1973
1974        // Parse optional derivation
1975        let derivation: Option<Derivation> = if let Some(d) = params.get("derivation") {
1976            Some(serde_json::from_value(d.clone()).map_err(|e| {
1977                BindingCoreError::validation(format!("Invalid 'derivation' field: {}", e))
1978            })?)
1979        } else {
1980            None
1981        };
1982
1983        // Parse optional policyContext
1984        let policy_context: Option<PolicyContext> = if let Some(p) = params.get("policyContext") {
1985            Some(serde_json::from_value(p.clone()).map_err(|e| {
1986                BindingCoreError::validation(format!("Invalid 'policyContext' field: {}", e))
1987            })?)
1988        } else {
1989            None
1990        };
1991
1992        let mut agent = self.lock()?;
1993        let jacs_doc = agent
1994            .create_attestation(
1995                &subject,
1996                &claims,
1997                &evidence,
1998                derivation.as_ref(),
1999                policy_context.as_ref(),
2000            )
2001            .map_err(|e| {
2002                BindingCoreError::document_failed(format!("Failed to create attestation: {}", e))
2003            })?;
2004
2005        serde_json::to_string_pretty(&jacs_doc.value).map_err(|e| {
2006            BindingCoreError::serialization_failed(format!(
2007                "Failed to serialize attestation: {}",
2008                e
2009            ))
2010        })
2011    }
2012
2013    /// Verify an attestation using local (crypto-only) verification.
2014    ///
2015    /// Takes the attestation document key in "id:version" format.
2016    /// Returns the verification result as a JSON string.
2017    #[cfg(feature = "attestation")]
2018    pub fn verify_attestation(&self, document_key: &str) -> BindingResult<String> {
2019        let agent = self.lock()?;
2020        let result = agent
2021            .verify_attestation_local_impl(document_key)
2022            .map_err(|e| {
2023                BindingCoreError::verification_failed(format!(
2024                    "Attestation local verification failed: {}",
2025                    e
2026                ))
2027            })?;
2028
2029        serde_json::to_string_pretty(&result).map_err(|e| {
2030            BindingCoreError::serialization_failed(format!(
2031                "Failed to serialize verification result: {}",
2032                e
2033            ))
2034        })
2035    }
2036
2037    /// Verify an attestation using full verification (evidence + chain).
2038    ///
2039    /// Takes the attestation document key in "id:version" format.
2040    /// Returns the verification result as a JSON string.
2041    #[cfg(feature = "attestation")]
2042    pub fn verify_attestation_full(&self, document_key: &str) -> BindingResult<String> {
2043        let agent = self.lock()?;
2044        let result = agent
2045            .verify_attestation_full_impl(document_key)
2046            .map_err(|e| {
2047                BindingCoreError::verification_failed(format!(
2048                    "Attestation full verification failed: {}",
2049                    e
2050                ))
2051            })?;
2052
2053        serde_json::to_string_pretty(&result).map_err(|e| {
2054            BindingCoreError::serialization_failed(format!(
2055                "Failed to serialize verification result: {}",
2056                e
2057            ))
2058        })
2059    }
2060
2061    /// Lift an existing signed JACS document into an attestation.
2062    ///
2063    /// Takes a signed document JSON string and claims JSON string.
2064    /// Returns the signed attestation document as a JSON string.
2065    #[cfg(feature = "attestation")]
2066    pub fn lift_to_attestation(
2067        &self,
2068        signed_doc_json: &str,
2069        claims_json: &str,
2070    ) -> BindingResult<String> {
2071        use jacs::attestation::types::Claim;
2072
2073        let claims: Vec<Claim> = serde_json::from_str(claims_json).map_err(|e| {
2074            BindingCoreError::serialization_failed(format!(
2075                "Failed to parse claims JSON: {}. \
2076                 Provide a valid JSON array of claim objects.",
2077                e
2078            ))
2079        })?;
2080
2081        let mut agent = self.lock()?;
2082        let jacs_doc =
2083            jacs::attestation::migration::lift_to_attestation(&mut agent, signed_doc_json, &claims)
2084                .map_err(|e| {
2085                    BindingCoreError::document_failed(format!(
2086                        "Failed to lift document to attestation: {}",
2087                        e
2088                    ))
2089                })?;
2090
2091        serde_json::to_string_pretty(&jacs_doc.value).map_err(|e| {
2092            BindingCoreError::serialization_failed(format!(
2093                "Failed to serialize attestation: {}",
2094                e
2095            ))
2096        })
2097    }
2098
2099    /// Export a signed attestation as a DSSE (Dead Simple Signing Envelope).
2100    ///
2101    /// Takes the attestation JSON string and returns a DSSE envelope JSON string.
2102    #[cfg(feature = "attestation")]
2103    pub fn export_attestation_dsse(&self, attestation_json: &str) -> BindingResult<String> {
2104        let att_value: Value = serde_json::from_str(attestation_json).map_err(|e| {
2105            BindingCoreError::serialization_failed(format!(
2106                "Failed to parse attestation JSON: {}",
2107                e
2108            ))
2109        })?;
2110
2111        let envelope = jacs::attestation::dsse::export_dsse(&att_value).map_err(|e| {
2112            BindingCoreError::document_failed(format!("Failed to export DSSE envelope: {}", e))
2113        })?;
2114
2115        serde_json::to_string_pretty(&envelope).map_err(|e| {
2116            BindingCoreError::serialization_failed(format!(
2117                "Failed to serialize DSSE envelope: {}",
2118                e
2119            ))
2120        })
2121    }
2122
2123    // =========================================================================
2124    // Protocol helpers (delegates to jacs::protocol)
2125    // =========================================================================
2126
2127    /// Build the JACS `Authorization` header value.
2128    ///
2129    /// Format: `"JACS {jacs_id}:{unix_timestamp}:{base64_signature}"`.
2130    /// Requires a loaded agent with keys.
2131    pub fn build_auth_header(&self) -> BindingResult<String> {
2132        let mut agent = self.lock()?;
2133        jacs::protocol::build_auth_header(&mut agent).map_err(|e| {
2134            BindingCoreError::signing_failed(format!("Failed to build auth header: {}", e))
2135        })
2136    }
2137
2138    /// Deterministically serialize a JSON string per RFC 8785 (JCS).
2139    ///
2140    /// Accepts a JSON string, parses it, and returns the canonicalized form.
2141    pub fn canonicalize_json(&self, json_string: &str) -> BindingResult<String> {
2142        let value: Value = serde_json::from_str(json_string).map_err(|e| {
2143            BindingCoreError::serialization_failed(format!(
2144                "Failed to parse JSON for canonicalization: {}",
2145                e
2146            ))
2147        })?;
2148        Ok(jacs::protocol::canonicalize_json(&value))
2149    }
2150
2151    /// Build and sign a JACS response envelope.
2152    ///
2153    /// Accepts a JSON payload string, returns a signed envelope JSON string
2154    /// containing `version`, `document_type`, `data`, `metadata`, and
2155    /// `jacsSignature`.
2156    pub fn sign_response(&self, payload_json: &str) -> BindingResult<String> {
2157        let mut agent = self.lock()?;
2158        let payload: Value = serde_json::from_str(payload_json).map_err(|e| {
2159            BindingCoreError::serialization_failed(format!(
2160                "Failed to parse payload JSON for sign_response: {}",
2161                e
2162            ))
2163        })?;
2164        let result = jacs::protocol::sign_response(&mut agent, &payload).map_err(|e| {
2165            BindingCoreError::signing_failed(format!("Failed to sign response: {}", e))
2166        })?;
2167        serde_json::to_string(&result).map_err(|e| {
2168            BindingCoreError::serialization_failed(format!(
2169                "Failed to serialize signed response: {}",
2170                e
2171            ))
2172        })
2173    }
2174
2175    /// Encode a document as URL-safe base64 (no padding) for verification.
2176    ///
2177    /// SDK clients use this to build verification URLs. JACS does not impose
2178    /// any URL structure — that is the SDK's responsibility.
2179    pub fn encode_verify_payload(&self, document: &str) -> BindingResult<String> {
2180        Ok(jacs::protocol::encode_verify_payload(document))
2181    }
2182
2183    /// Decode a URL-safe base64 verification payload back to the original
2184    /// document string.
2185    pub fn decode_verify_payload(&self, encoded: &str) -> BindingResult<String> {
2186        jacs::protocol::decode_verify_payload(encoded).map_err(|e| {
2187            BindingCoreError::serialization_failed(format!(
2188                "Failed to decode verify payload: {}",
2189                e
2190            ))
2191        })
2192    }
2193
2194    /// Extract the document ID from a JACS-signed document.
2195    ///
2196    /// Checks `jacsDocumentId`, `document_id`, `id` in priority order.
2197    /// SDK clients use this to build hosted verification URLs.
2198    pub fn extract_document_id(&self, document: &str) -> BindingResult<String> {
2199        jacs::protocol::extract_document_id(document)
2200            .map_err(|e| BindingCoreError::generic(format!("Failed to extract document ID: {}", e)))
2201    }
2202
2203    /// Unwrap a JACS-signed event, verifying the signature when the signer's
2204    /// public key is known.
2205    ///
2206    /// `event_json` is the signed event as a JSON string.
2207    /// `server_keys_json` is a JSON object mapping agent IDs to base64-encoded
2208    /// public key bytes: `{"agent_id": "base64_key", ...}`.
2209    ///
2210    /// Returns a JSON string: `{"data": <unwrapped>, "verified": <bool>}`.
2211    pub fn unwrap_signed_event(
2212        &self,
2213        event_json: &str,
2214        server_keys_json: &str,
2215    ) -> BindingResult<String> {
2216        let agent = self.lock()?;
2217        let event: Value = serde_json::from_str(event_json).map_err(|e| {
2218            BindingCoreError::serialization_failed(format!(
2219                "Failed to parse event JSON for unwrap_signed_event: {}",
2220                e
2221            ))
2222        })?;
2223        let keys_map: HashMap<String, String> =
2224            serde_json::from_str(server_keys_json).map_err(|e| {
2225                BindingCoreError::serialization_failed(format!(
2226                    "Failed to parse server keys JSON for unwrap_signed_event: {}",
2227                    e
2228                ))
2229            })?;
2230        let keys: HashMap<String, Vec<u8>> = keys_map
2231            .into_iter()
2232            .map(|(k, v)| {
2233                let bytes = base64::engine::general_purpose::STANDARD
2234                    .decode(&v)
2235                    .unwrap_or_else(|_| v.into_bytes());
2236                (k, bytes)
2237            })
2238            .collect();
2239        let (data, verified) =
2240            jacs::protocol::unwrap_signed_event(&agent, &event, &keys).map_err(|e| {
2241                BindingCoreError::verification_failed(format!(
2242                    "Failed to unwrap signed event: {}",
2243                    e
2244                ))
2245            })?;
2246        let result = json!({"data": data, "verified": verified});
2247        serde_json::to_string(&result).map_err(|e| {
2248            BindingCoreError::serialization_failed(format!(
2249                "Failed to serialize unwrap_signed_event result: {}",
2250                e
2251            ))
2252        })
2253    }
2254}
2255
2256// =============================================================================
2257// Standalone diagnostics (no agent required)
2258// =============================================================================
2259
2260/// Returns basic JACS diagnostic info as a pretty-printed JSON string.
2261/// Does not require a loaded agent.
2262pub fn diagnostics_standalone() -> String {
2263    serde_json::to_string_pretty(&jacs::simple::diagnostics()).unwrap_or_default()
2264}
2265
2266// =============================================================================
2267// Standalone verification (no agent required)
2268// =============================================================================
2269
2270/// Result of verifying a signed JACS document (used by verify_document_standalone).
2271#[derive(Debug, Clone)]
2272pub struct VerificationResult {
2273    /// Whether the document's signature and hash are valid.
2274    pub valid: bool,
2275    /// The signer's agent ID from the document's jacsSignature.agentID (empty if unparseable).
2276    pub signer_id: String,
2277    /// The signing timestamp from jacsSignature.date (empty if unparseable).
2278    pub timestamp: String,
2279    /// The signer's agent version from jacsSignature.agentVersion (empty if unparseable).
2280    pub agent_version: String,
2281}
2282
2283/// Verify a signed JACS document without loading an agent.
2284///
2285/// Creates a minimal verifier context (config with data/key directories and optional
2286/// key resolution), runs verification, and returns a result with valid flag and signer_id.
2287/// Does not persist any state.
2288///
2289/// # Arguments
2290///
2291/// * `signed_document` - Full signed JACS document JSON string.
2292/// * `key_resolution` - Optional key resolution order, e.g. "local" or "local,remote" (default "local").
2293/// * `data_directory` - Optional path for data/trust store (defaults to temp/cwd).
2294/// * `key_directory` - Optional path for public keys (defaults to temp/cwd).
2295///
2296/// # Returns
2297///
2298/// * `Ok(VerificationResult { valid: true, signer_id })` when signature and hash are valid.
2299/// * `Ok(VerificationResult { valid: false, signer_id })` when document parses but verification fails.
2300/// * `Err` when setup fails (e.g. missing key directory when using local resolution).
2301pub fn verify_document_standalone(
2302    signed_document: &str,
2303    key_resolution: Option<&str>,
2304    data_directory: Option<&str>,
2305    key_directory: Option<&str>,
2306) -> BindingResult<VerificationResult> {
2307    use std::collections::HashSet;
2308    use std::path::{Path, PathBuf};
2309    use std::sync::{Mutex, OnceLock};
2310
2311    fn absolutize_dir(raw: &str) -> String {
2312        let p = PathBuf::from(raw);
2313        if p.is_absolute() {
2314            p.to_string_lossy().to_string()
2315        } else {
2316            std::env::current_dir()
2317                .unwrap_or_else(|_| PathBuf::from("."))
2318                .join(p)
2319                .to_string_lossy()
2320                .to_string()
2321        }
2322    }
2323
2324    fn sig_field(doc: &str, field: &str) -> String {
2325        serde_json::from_str::<Value>(doc)
2326            .ok()
2327            .and_then(|v| {
2328                v.get("jacsSignature")
2329                    .and_then(|s| s.get(field))
2330                    .and_then(|f| f.as_str())
2331                    .map(String::from)
2332            })
2333            .unwrap_or_default()
2334    }
2335
2336    fn has_local_key_cache(root: &Path, key_hash: &str) -> bool {
2337        if key_hash.is_empty() {
2338            return false;
2339        }
2340        root.join("public_keys")
2341            .join(format!("{}.pem", key_hash))
2342            .exists()
2343            && root
2344                .join("public_keys")
2345                .join(format!("{}.enc_type", key_hash))
2346                .exists()
2347    }
2348
2349    fn build_fixture_key_cache(cache_root: &Path, source_dirs: &[PathBuf]) -> usize {
2350        let public_keys_dir = cache_root.join("public_keys");
2351        if std::fs::create_dir_all(&public_keys_dir).is_err() {
2352            return 0;
2353        }
2354
2355        let mut written: HashSet<String> = HashSet::new();
2356        for dir in source_dirs {
2357            let entries = match std::fs::read_dir(dir) {
2358                Ok(v) => v,
2359                Err(_) => continue,
2360            };
2361
2362            for entry in entries.flatten() {
2363                let path = entry.path();
2364                if !path.is_file() {
2365                    continue;
2366                }
2367                let Some(name) = path.file_name().and_then(|s| s.to_str()) else {
2368                    continue;
2369                };
2370                let Some(prefix) = name.strip_suffix("_metadata.json") else {
2371                    continue;
2372                };
2373
2374                let metadata = match std::fs::read_to_string(&path)
2375                    .ok()
2376                    .and_then(|s| serde_json::from_str::<Value>(&s).ok())
2377                {
2378                    Some(v) => v,
2379                    None => continue,
2380                };
2381                let key_hash = metadata
2382                    .get("public_key_hash")
2383                    .and_then(|v| v.as_str())
2384                    .unwrap_or("")
2385                    .trim();
2386                let signing_algorithm = metadata
2387                    .get("signing_algorithm")
2388                    .and_then(|v| v.as_str())
2389                    .unwrap_or("")
2390                    .trim();
2391                if key_hash.is_empty() || signing_algorithm.is_empty() {
2392                    continue;
2393                }
2394                if written.contains(key_hash) {
2395                    continue;
2396                }
2397
2398                let key_path = dir.join(format!("{}_public_key.pem", prefix));
2399                let key_bytes = match std::fs::read(&key_path) {
2400                    Ok(v) => v,
2401                    Err(_) => continue,
2402                };
2403
2404                if std::fs::write(public_keys_dir.join(format!("{}.pem", key_hash)), key_bytes)
2405                    .is_err()
2406                {
2407                    continue;
2408                }
2409                if std::fs::write(
2410                    public_keys_dir.join(format!("{}.enc_type", key_hash)),
2411                    signing_algorithm.as_bytes(),
2412                )
2413                .is_err()
2414                {
2415                    continue;
2416                }
2417
2418                written.insert(key_hash.to_string());
2419            }
2420        }
2421
2422        written.len()
2423    }
2424
2425    fn standalone_verify_lock() -> &'static Mutex<()> {
2426        static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
2427        LOCK.get_or_init(|| Mutex::new(()))
2428    }
2429
2430    let _lock = standalone_verify_lock()
2431        .lock()
2432        .map_err(|e| BindingCoreError::generic(format!("Failed to lock standalone verify: {e}")))?;
2433
2434    let signer_id = sig_field(signed_document, "agentID");
2435    let timestamp = sig_field(signed_document, "date");
2436    let agent_version = sig_field(signed_document, "agentVersion");
2437    let signer_public_key_hash = sig_field(signed_document, "publicKeyHash");
2438
2439    // Always resolve caller-provided directories to absolute paths so relative
2440    // inputs like "../fixtures" work regardless of process CWD.
2441    let temp_dir = std::env::temp_dir().to_string_lossy().to_string();
2442    let raw_data_dir = data_directory
2443        .map(String::from)
2444        .unwrap_or_else(|| temp_dir.clone());
2445    let raw_key_dir = key_directory
2446        .map(String::from)
2447        .unwrap_or_else(|| raw_data_dir.clone());
2448
2449    let absolute_data_dir = absolutize_dir(&raw_data_dir);
2450    let absolute_key_dir = absolutize_dir(&raw_key_dir);
2451
2452    // Verification loads public keys from {data_directory}/public_keys.
2453    // If only key_directory is supplied, use it as the storage root fallback.
2454    let mut effective_storage_root = if data_directory.is_some() {
2455        absolute_data_dir.clone()
2456    } else if key_directory.is_some() {
2457        absolute_key_dir.clone()
2458    } else {
2459        absolute_data_dir.clone()
2460    };
2461    let mut temp_cache_root: Option<PathBuf> = None;
2462
2463    // Many cross-language fixture directories store keys as:
2464    //   <prefix>_metadata.json + <prefix>_public_key.pem
2465    // rather than public_keys/{hash}.pem.
2466    // Build a deterministic temp cache when local key files are missing.
2467    let local_requested = key_resolution.map_or(true, |kr| {
2468        kr.split(',')
2469            .any(|part| part.trim().eq_ignore_ascii_case("local"))
2470    });
2471    if local_requested && !signer_public_key_hash.is_empty() {
2472        let current_root = PathBuf::from(&effective_storage_root);
2473        if !has_local_key_cache(&current_root, &signer_public_key_hash) {
2474            let mut source_dirs = Vec::new();
2475            let data_path = PathBuf::from(&absolute_data_dir);
2476            let key_path = PathBuf::from(&absolute_key_dir);
2477            if data_path.exists() {
2478                source_dirs.push(data_path);
2479            }
2480            if key_path.exists() && !source_dirs.iter().any(|p| p == &key_path) {
2481                source_dirs.push(key_path);
2482            }
2483
2484            if !source_dirs.is_empty() {
2485                let nonce = std::time::SystemTime::now()
2486                    .duration_since(std::time::UNIX_EPOCH)
2487                    .map(|d| d.as_nanos())
2488                    .unwrap_or(0);
2489                let cache_root = std::env::temp_dir().join(format!(
2490                    "jacs_standalone_keycache_{}_{}",
2491                    std::process::id(),
2492                    nonce
2493                ));
2494                let _ = build_fixture_key_cache(&cache_root, &source_dirs);
2495                if has_local_key_cache(&cache_root, &signer_public_key_hash) {
2496                    effective_storage_root = cache_root.to_string_lossy().to_string();
2497                    temp_cache_root = Some(cache_root);
2498                } else {
2499                    let _ = std::fs::remove_dir_all(&cache_root);
2500                }
2501            }
2502        }
2503    }
2504    let explicit_local_key_available = local_requested
2505        && !signer_public_key_hash.is_empty()
2506        && has_local_key_cache(
2507            &PathBuf::from(&effective_storage_root),
2508            &signer_public_key_hash,
2509        );
2510
2511    // Re-root storage and keep config dirs empty so path construction stays
2512    // relative to storage root (e.g. "public_keys/<hash>.pem").
2513    let data_dir = String::new();
2514    let key_dir = String::new();
2515
2516    let config = Config::new(
2517        Some("false".to_string()),
2518        Some(data_dir.clone()),
2519        Some(key_dir.clone()),
2520        Some("jacs.private.pem.enc".to_string()),
2521        Some("jacs.public.pem".to_string()),
2522        Some("pq2025".to_string()),
2523        None,
2524        Some("".to_string()),
2525        Some("fs".to_string()),
2526    );
2527    let config_json = serde_json::to_string_pretty(&config).map_err(|e| {
2528        BindingCoreError::serialization_failed(format!("Failed to serialize config: {}", e))
2529    })?;
2530
2531    let thread_id = format!("{:?}", std::thread::current().id())
2532        .chars()
2533        .map(|c| if c.is_ascii_alphanumeric() { c } else { '_' })
2534        .collect::<String>();
2535    let nonce = std::time::SystemTime::now()
2536        .duration_since(std::time::UNIX_EPOCH)
2537        .map(|d| d.as_nanos())
2538        .unwrap_or(0);
2539    let config_path = std::env::temp_dir().join(format!(
2540        "jacs_standalone_verify_config_{}_{}_{}.json",
2541        std::process::id(),
2542        thread_id,
2543        nonce
2544    ));
2545    std::fs::write(&config_path, &config_json)
2546        .map_err(|e| BindingCoreError::generic(format!("Failed to write temp config: {}", e)))?;
2547
2548    // Issue 008: Use load_file_only to bypass env/jenv overrides entirely.
2549    // This eliminates the 16-key JenvGuard save/clear/restore pattern.
2550    // The config file is authoritative — no ambient JACS_* vars can pollute it.
2551    //
2552    // JACS_KEY_RESOLUTION is the only runtime-read jenv key we still need to
2553    // manage, since key_resolution_order() reads it at verification time.
2554    use jacs::storage::jenv;
2555
2556    // Minimal guard for JACS_KEY_RESOLUTION only (Issue 014-safe).
2557    struct KeyResolutionGuard {
2558        had_override: bool,
2559        prev_value: Option<String>,
2560    }
2561    impl Drop for KeyResolutionGuard {
2562        fn drop(&mut self) {
2563            if self.had_override {
2564                if let Some(ref val) = self.prev_value {
2565                    let _ = jacs::storage::jenv::set_env_var("JACS_KEY_RESOLUTION", val);
2566                } else {
2567                    let _ = jacs::storage::jenv::clear_env_var("JACS_KEY_RESOLUTION");
2568                }
2569            } else {
2570                let _ = jacs::storage::jenv::clear_env_var("JACS_KEY_RESOLUTION");
2571            }
2572        }
2573    }
2574    let kr_had_override = jenv::has_jenv_override("JACS_KEY_RESOLUTION");
2575    let kr_prev = if kr_had_override {
2576        jenv::get_env_var("JACS_KEY_RESOLUTION", false)
2577            .ok()
2578            .flatten()
2579    } else {
2580        None
2581    };
2582    if let Some(kr) = key_resolution {
2583        let _ = jenv::set_env_var("JACS_KEY_RESOLUTION", kr);
2584    } else {
2585        let _ = jenv::clear_env_var("JACS_KEY_RESOLUTION");
2586    }
2587    let _kr_guard = KeyResolutionGuard {
2588        had_override: kr_had_override,
2589        prev_value: kr_prev,
2590    };
2591
2592    let result: BindingResult<VerificationResult> = (|| {
2593        let wrapper = AgentWrapper::new();
2594        wrapper.load_file_only(config_path.to_string_lossy().to_string())?;
2595        let _ = wrapper.set_storage_root(PathBuf::from(&effective_storage_root));
2596
2597        if explicit_local_key_available {
2598            let key_base = PathBuf::from(&effective_storage_root)
2599                .join("public_keys")
2600                .join(&signer_public_key_hash);
2601            let public_key = std::fs::read(key_base.with_extension("pem")).map_err(|e| {
2602                BindingCoreError::verification_failed(format!(
2603                    "Failed to load local public key for hash '{}': {}",
2604                    signer_public_key_hash, e
2605                ))
2606            })?;
2607            let enc_type = std::fs::read_to_string(key_base.with_extension("enc_type"))
2608                .map_err(|e| {
2609                    BindingCoreError::verification_failed(format!(
2610                        "Failed to load local public key type for hash '{}': {}",
2611                        signer_public_key_hash, e
2612                    ))
2613                })?
2614                .trim()
2615                .to_string();
2616
2617            let mut agent = wrapper.lock()?;
2618            let doc = agent.load_document(signed_document).map_err(|e| {
2619                BindingCoreError::document_failed(format!("Failed to load document: {}", e))
2620            })?;
2621            let document_key = doc.getkey();
2622            let value = doc.getvalue();
2623            agent.verify_hash(value).map_err(|e| {
2624                BindingCoreError::verification_failed(format!(
2625                    "Failed to verify document hash: {}",
2626                    e
2627                ))
2628            })?;
2629            agent
2630                .verify_document_signature(
2631                    &document_key,
2632                    None,
2633                    None,
2634                    Some(public_key),
2635                    Some(enc_type.clone()),
2636                )
2637                .map_err(|e| {
2638                    BindingCoreError::verification_failed(format!(
2639                        "Failed to verify document signature (enc_type={}): {}",
2640                        enc_type, e
2641                    ))
2642                })?;
2643
2644            return Ok(VerificationResult {
2645                valid: true,
2646                signer_id: signer_id.clone(),
2647                timestamp: timestamp.clone(),
2648                agent_version: agent_version.clone(),
2649            });
2650        }
2651
2652        let valid = wrapper.verify_document(signed_document)?;
2653        Ok(VerificationResult {
2654            valid,
2655            signer_id: signer_id.clone(),
2656            timestamp: timestamp.clone(),
2657            agent_version: agent_version.clone(),
2658        })
2659    })();
2660
2661    // Clean up temp config file
2662    let _ = std::fs::remove_file(&config_path);
2663    if let Some(cache_root) = temp_cache_root {
2664        let _ = std::fs::remove_dir_all(cache_root);
2665    }
2666
2667    match result {
2668        Ok(r) => Ok(r),
2669        Err(e) => {
2670            if e.kind == ErrorKind::VerificationFailed
2671                || e.kind == ErrorKind::DocumentFailed
2672                || e.kind == ErrorKind::InvalidArgument
2673            {
2674                Ok(VerificationResult {
2675                    valid: false,
2676                    signer_id,
2677                    timestamp,
2678                    agent_version,
2679                })
2680            } else {
2681                Err(e)
2682            }
2683        }
2684    }
2685}
2686
2687// =============================================================================
2688// Stateless Utility Functions
2689// =============================================================================
2690
2691/// Hash a string using the JACS hash function (SHA-256).
2692pub fn hash_string(data: &str) -> String {
2693    jacs_hash_string(&data.to_string())
2694}
2695
2696/// Hash a base64-encoded public key using Rust-owned public-key hashing rules.
2697pub fn hash_public_key_base64(public_key_b64: &str) -> BindingResult<String> {
2698    let public_key = decode_public_key_base64(public_key_b64)?;
2699    Ok(jacs::crypt::hash::hash_public_key(&public_key))
2700}
2701
2702/// Build a JWK set from a base64-encoded public key using Rust-owned parsing rules.
2703pub fn build_jwk_set_from_public_key(
2704    public_key_b64: &str,
2705    key_algorithm: &str,
2706    key_id: &str,
2707) -> BindingResult<String> {
2708    let public_key = decode_public_key_base64(public_key_b64)?;
2709    let jwk_set = build_jwk_set_from_public_key_bytes(&public_key, key_algorithm, key_id)?;
2710    serde_json::to_string(&jwk_set).map_err(|e| {
2711        BindingCoreError::serialization_failed(format!("Failed to serialize JWK set: {}", e))
2712    })
2713}
2714
2715/// Enforce the Rust-owned network access policy for a named capability.
2716pub fn ensure_network_access(capability: &str) -> BindingResult<()> {
2717    let capability = jacs::config::NetworkCapability::from_str(capability)
2718        .map_err(BindingCoreError::invalid_argument)?;
2719    jacs::config::ensure_network_access(capability)
2720        .map_err(|e| BindingCoreError::network_failed(e.to_string()))
2721}
2722
2723/// Fetch an A2A Agent Card JSON object using Rust-owned network policy and HTTP behavior.
2724pub fn fetch_agent_card(base_url: &str, timeout_ms: Option<u64>) -> BindingResult<String> {
2725    let trimmed = base_url.trim();
2726    if trimmed.is_empty() {
2727        return Err(BindingCoreError::invalid_argument(
2728            "Agent base URL cannot be empty",
2729        ));
2730    }
2731
2732    let card_url = format!(
2733        "{}/.well-known/agent-card.json",
2734        trimmed.trim_end_matches('/')
2735    );
2736    let parsed_url = Url::parse(&card_url).map_err(|e| {
2737        BindingCoreError::invalid_argument(format!("Invalid agent URL '{}': {}", base_url, e))
2738    })?;
2739    validate_network_url(&parsed_url, "Agent Card URL")?;
2740    jacs::config::ensure_network_access(jacs::config::NetworkCapability::AgentCardFetch)
2741        .map_err(|e| BindingCoreError::network_failed(e.to_string()))?;
2742
2743    let client = build_blocking_json_client(timeout_ms.unwrap_or(DEFAULT_NETWORK_TIMEOUT_MS))?;
2744    let response = client
2745        .get(parsed_url.clone())
2746        .header(ACCEPT, "application/json")
2747        .send()
2748        .map_err(|e| {
2749            if e.is_timeout() {
2750                BindingCoreError::network_failed(format!(
2751                    "Agent discovery timed out: {}",
2752                    parsed_url
2753                ))
2754            } else {
2755                BindingCoreError::network_failed(format!(
2756                    "Agent unreachable: {} ({})",
2757                    parsed_url, e
2758                ))
2759            }
2760        })?;
2761
2762    if response.status() == StatusCode::NOT_FOUND {
2763        return Err(BindingCoreError::network_failed(format!(
2764            "Agent card not found (404): {}",
2765            parsed_url
2766        )));
2767    }
2768
2769    if !response.status().is_success() {
2770        return Err(BindingCoreError::network_failed(format!(
2771            "Agent card request failed (HTTP {}): {}",
2772            response.status(),
2773            parsed_url
2774        )));
2775    }
2776
2777    let content_type = content_type_header(&response);
2778    if !content_type.is_empty() && !content_type.to_ascii_lowercase().contains("json") {
2779        return Err(BindingCoreError::validation(format!(
2780            "Agent card response is not JSON (content-type: {}): {}",
2781            content_type, parsed_url
2782        )));
2783    }
2784
2785    let body = response.text().map_err(|e| {
2786        BindingCoreError::network_failed(format!(
2787            "Failed to read Agent Card response from {}: {}",
2788            parsed_url, e
2789        ))
2790    })?;
2791
2792    parse_json_object_body(
2793        &body,
2794        format!("Agent card is not valid JSON: {}", parsed_url),
2795        format!("Agent card at {} is not a JSON object", parsed_url),
2796    )
2797}
2798
2799/// Fetch a remote key lookup JSON object using Rust-owned network policy and HTTP behavior.
2800pub fn fetch_remote_key_lookup(
2801    base_url: Option<&str>,
2802    jacs_id: Option<&str>,
2803    version: Option<&str>,
2804    public_key_hash: Option<&str>,
2805    timeout_ms: Option<u64>,
2806) -> BindingResult<String> {
2807    let resolved_base_url = resolve_keys_base_url(base_url);
2808    let mut parsed_url = Url::parse(&resolved_base_url).map_err(|e| {
2809        BindingCoreError::invalid_argument(format!(
2810            "Invalid JACS key base URL '{}': {}",
2811            resolved_base_url, e
2812        ))
2813    })?;
2814    validate_network_url(&parsed_url, "JACS key lookup base URL")?;
2815
2816    {
2817        let mut segments = parsed_url.path_segments_mut().map_err(|_| {
2818            BindingCoreError::invalid_argument(format!(
2819                "Invalid JACS key base URL '{}': cannot append path segments",
2820                resolved_base_url
2821            ))
2822        })?;
2823
2824        if let Some(hash) = public_key_hash
2825            .map(str::trim)
2826            .filter(|value| !value.is_empty())
2827        {
2828            let normalized_hash = normalize_public_key_hash(hash)?;
2829            segments.extend(["jacs", "v1", "keys", "by-hash"]);
2830            segments.push(&normalized_hash);
2831        } else {
2832            let agent_id = jacs_id
2833                .map(str::trim)
2834                .filter(|value| !value.is_empty())
2835                .ok_or_else(|| {
2836                    BindingCoreError::invalid_argument(
2837                        "fetch_remote_key_lookup requires jacs_id or public_key_hash",
2838                    )
2839                })?;
2840            let resolved_version = version
2841                .map(str::trim)
2842                .filter(|value| !value.is_empty())
2843                .unwrap_or("latest");
2844            segments.extend(["jacs", "v1", "agents"]);
2845            segments.push(agent_id);
2846            segments.push("keys");
2847            segments.push(resolved_version);
2848        }
2849    }
2850
2851    jacs::config::ensure_network_access(jacs::config::NetworkCapability::RemoteKeyFetch)
2852        .map_err(|e| BindingCoreError::network_failed(e.to_string()))?;
2853
2854    let client = build_blocking_json_client(timeout_ms.unwrap_or(DEFAULT_NETWORK_TIMEOUT_MS))?;
2855    let response = client
2856        .get(parsed_url.clone())
2857        .header(ACCEPT, "application/json")
2858        .send()
2859        .map_err(|e| {
2860            if e.is_timeout() {
2861                BindingCoreError::network_failed(format!(
2862                    "Remote key lookup timed out: {}",
2863                    parsed_url
2864                ))
2865            } else {
2866                BindingCoreError::network_failed(format!(
2867                    "Failed to reach key lookup endpoint {}: {}",
2868                    parsed_url, e
2869                ))
2870            }
2871        })?;
2872
2873    let status = response.status();
2874    let content_type = content_type_header(&response);
2875    let body = response.text().map_err(|e| {
2876        BindingCoreError::network_failed(format!(
2877            "Failed to read key lookup response from {}: {}",
2878            parsed_url, e
2879        ))
2880    })?;
2881
2882    if !status.is_success() {
2883        let detail = if body.trim().is_empty() {
2884            status.canonical_reason().unwrap_or("unknown error")
2885        } else {
2886            body.trim()
2887        };
2888        return Err(BindingCoreError::network_failed(format!(
2889            "HTTP {} from key lookup endpoint: {}",
2890            status.as_u16(),
2891            detail
2892        )));
2893    }
2894
2895    if !content_type.is_empty() && !content_type.to_ascii_lowercase().contains("json") {
2896        return Err(BindingCoreError::validation(format!(
2897            "Key lookup endpoint returned non-JSON response: {}",
2898            parsed_url
2899        )));
2900    }
2901
2902    parse_json_object_body(
2903        &body,
2904        format!(
2905            "Key lookup endpoint returned non-JSON response: {}",
2906            parsed_url
2907        ),
2908        format!(
2909            "Key lookup endpoint returned a non-object response: {}",
2910            parsed_url
2911        ),
2912    )
2913}
2914
2915/// Resolve a private-key password using Rust-owned env, keychain, and filesystem rules.
2916///
2917/// Returns an empty string when no password source is available.
2918pub fn resolve_private_key_password(
2919    config_path: Option<&str>,
2920    key_directory: Option<&str>,
2921    explicit_password: Option<&str>,
2922) -> BindingResult<String> {
2923    if let Some(password) = explicit_password {
2924        if password.trim().is_empty() {
2925            return Err(BindingCoreError::invalid_argument(
2926                "Explicit password provided but empty or whitespace-only.",
2927            ));
2928        }
2929        return Ok(password.to_string());
2930    }
2931
2932    if let Ok(password) = std::env::var("JACS_PRIVATE_KEY_PASSWORD") {
2933        if password.trim().is_empty() {
2934            return Err(BindingCoreError::invalid_argument(
2935                "JACS_PRIVATE_KEY_PASSWORD is set but empty or whitespace-only.",
2936            ));
2937        }
2938        return Ok(password);
2939    }
2940
2941    let (resolved_key_directory, agent_id) = resolve_password_context(config_path, key_directory)?;
2942
2943    match jacs::crypt::aes_encrypt::resolve_private_key_password(None, agent_id.as_deref()) {
2944        Ok(password) => Ok(password),
2945        Err(e) if missing_password_message(&e.to_string()) => Ok(read_password_file(
2946            &resolved_key_directory.join(".jacs_password"),
2947        )?
2948        .unwrap_or_default()),
2949        Err(e) => Err(BindingCoreError::generic(format!(
2950            "Failed to resolve private key password: {}",
2951            e
2952        ))),
2953    }
2954}
2955
2956/// Resolve an existing password for quickstart, or generate and optionally persist one in Rust.
2957pub fn quickstart_private_key_password(
2958    config_path: Option<&str>,
2959    key_directory: Option<&str>,
2960) -> BindingResult<String> {
2961    let existing = resolve_private_key_password(config_path, key_directory, None)?;
2962    if !existing.is_empty() {
2963        return Ok(existing);
2964    }
2965
2966    let password = generate_private_key_password_value();
2967    if truthy_env_var("JACS_SAVE_PASSWORD_FILE") {
2968        let (resolved_key_directory, _agent_id) =
2969            resolve_password_context(config_path, key_directory)?;
2970        persist_password_file(&resolved_key_directory, &password)?;
2971    }
2972
2973    Ok(password)
2974}
2975
2976/// Create a JACS configuration JSON string.
2977pub fn create_config(
2978    jacs_use_security: Option<String>,
2979    jacs_data_directory: Option<String>,
2980    jacs_key_directory: Option<String>,
2981    jacs_agent_private_key_filename: Option<String>,
2982    jacs_agent_public_key_filename: Option<String>,
2983    jacs_agent_key_algorithm: Option<String>,
2984    jacs_private_key_password: Option<String>,
2985    jacs_agent_id_and_version: Option<String>,
2986    jacs_default_storage: Option<String>,
2987) -> BindingResult<String> {
2988    let config = Config::new(
2989        jacs_use_security,
2990        jacs_data_directory,
2991        jacs_key_directory,
2992        jacs_agent_private_key_filename,
2993        jacs_agent_public_key_filename,
2994        jacs_agent_key_algorithm,
2995        jacs_private_key_password,
2996        jacs_agent_id_and_version,
2997        jacs_default_storage,
2998    );
2999
3000    serde_json::to_string_pretty(&config).map_err(|e| {
3001        BindingCoreError::serialization_failed(format!("Failed to serialize config: {}", e))
3002    })
3003}
3004
3005// =============================================================================
3006// Trust Store Functions
3007// =============================================================================
3008
3009/// Add an agent to the local trust store.
3010pub fn trust_agent(agent_json: &str) -> BindingResult<String> {
3011    jacs::trust::trust_agent(agent_json)
3012        .map_err(|e| BindingCoreError::trust_failed(format!("Failed to trust agent: {}", e)))
3013}
3014
3015/// Add an agent to the local trust store using an explicitly provided public key.
3016///
3017/// This is the recommended first-contact bootstrap for secure trust establishment.
3018pub fn trust_agent_with_key(agent_json: &str, public_key_pem: &str) -> BindingResult<String> {
3019    if public_key_pem.trim().is_empty() {
3020        return Err(BindingCoreError::invalid_argument(
3021            "public_key_pem cannot be empty",
3022        ));
3023    }
3024    jacs::trust::trust_agent_with_key(agent_json, Some(public_key_pem)).map_err(|e| {
3025        BindingCoreError::trust_failed(format!("Failed to trust agent with explicit key: {}", e))
3026    })
3027}
3028
3029/// List all trusted agent IDs.
3030pub fn list_trusted_agents() -> BindingResult<Vec<String>> {
3031    jacs::trust::list_trusted_agents().map_err(|e| {
3032        BindingCoreError::trust_failed(format!("Failed to list trusted agents: {}", e))
3033    })
3034}
3035
3036/// Remove an agent from the trust store.
3037pub fn untrust_agent(agent_id: &str) -> BindingResult<()> {
3038    jacs::trust::untrust_agent(agent_id)
3039        .map_err(|e| BindingCoreError::trust_failed(format!("Failed to untrust agent: {}", e)))
3040}
3041
3042/// Check if an agent is in the trust store.
3043pub fn is_trusted(agent_id: &str) -> bool {
3044    jacs::trust::is_trusted(agent_id)
3045}
3046
3047/// Get a trusted agent's JSON document.
3048pub fn get_trusted_agent(agent_id: &str) -> BindingResult<String> {
3049    jacs::trust::get_trusted_agent(agent_id)
3050        .map_err(|e| BindingCoreError::trust_failed(format!("Failed to get trusted agent: {}", e)))
3051}
3052
3053// =============================================================================
3054// Audit (security audit and health checks)
3055// =============================================================================
3056
3057/// Run a read-only security audit and health checks.
3058///
3059/// Returns the audit result as a JSON string (risks, health_checks, summary).
3060/// Does not modify state. Optional config path and recent document re-verification count.
3061pub fn audit(config_path: Option<&str>, recent_n: Option<u32>) -> BindingResult<String> {
3062    use jacs::audit::{AuditOptions, audit as jacs_audit};
3063
3064    let mut opts = AuditOptions::default();
3065    opts.config_path = config_path.map(String::from);
3066    if let Some(n) = recent_n {
3067        opts.recent_verify_count = Some(n);
3068    }
3069    let result =
3070        jacs_audit(opts).map_err(|e| BindingCoreError::generic(format!("Audit failed: {}", e)))?;
3071    serde_json::to_string_pretty(&result).map_err(|e| {
3072        BindingCoreError::serialization_failed(format!("Failed to serialize audit result: {}", e))
3073    })
3074}
3075
3076// =============================================================================
3077// CLI Utility Functions
3078// =============================================================================
3079
3080/// Create a JACS agent programmatically (non-interactive).
3081///
3082/// Accepts all creation parameters and returns a JSON string containing agent info.
3083pub fn create_agent_programmatic(
3084    name: &str,
3085    password: &str,
3086    algorithm: Option<&str>,
3087    data_directory: Option<&str>,
3088    key_directory: Option<&str>,
3089    config_path: Option<&str>,
3090    agent_type: Option<&str>,
3091    description: Option<&str>,
3092    domain: Option<&str>,
3093    default_storage: Option<&str>,
3094) -> BindingResult<String> {
3095    use jacs::simple::{CreateAgentParams, SimpleAgent};
3096
3097    let params = CreateAgentParams {
3098        name: name.to_string(),
3099        password: password.to_string(),
3100        algorithm: algorithm.unwrap_or("pq2025").to_string(),
3101        data_directory: data_directory.unwrap_or("./jacs_data").to_string(),
3102        key_directory: key_directory.unwrap_or("./jacs_keys").to_string(),
3103        config_path: config_path.unwrap_or("./jacs.config.json").to_string(),
3104        agent_type: agent_type.unwrap_or("ai").to_string(),
3105        description: description.unwrap_or("").to_string(),
3106        domain: domain.unwrap_or("").to_string(),
3107        default_storage: default_storage.unwrap_or("fs").to_string(),
3108        storage: None,
3109    };
3110
3111    let (_agent, info) = SimpleAgent::create_with_params(params)
3112        .map_err(|e| BindingCoreError::agent_load(format!("Failed to create agent: {}", e)))?;
3113
3114    serde_json::to_string_pretty(&info).map_err(|e| {
3115        BindingCoreError::serialization_failed(format!("Failed to serialize agent info: {}", e))
3116    })
3117}
3118
3119/// Create agent and config files interactively.
3120pub fn handle_agent_create(filename: Option<&String>, create_keys: bool) -> BindingResult<()> {
3121    jacs::cli_utils::create::handle_agent_create(filename, create_keys)
3122        .map_err(|e| BindingCoreError::generic(e.to_string()))
3123}
3124
3125/// Like `handle_agent_create` but auto-updates config with the agent ID when
3126/// `auto_update_config` is true, skipping the interactive prompt.
3127pub fn handle_agent_create_auto(
3128    filename: Option<&String>,
3129    create_keys: bool,
3130    auto_update_config: bool,
3131) -> BindingResult<()> {
3132    jacs::cli_utils::create::handle_agent_create_auto(filename, create_keys, auto_update_config)
3133        .map_err(|e| BindingCoreError::generic(e.to_string()))
3134}
3135
3136/// Create a jacs.config.json file interactively.
3137pub fn handle_config_create() -> BindingResult<()> {
3138    jacs::cli_utils::create::handle_config_create()
3139        .map_err(|e| BindingCoreError::generic(e.to_string()))
3140}
3141
3142// =============================================================================
3143// DNS Verification
3144// =============================================================================
3145
3146/// Re-export DNS verification result for bindings.
3147pub use jacs::dns::bootstrap::DnsVerificationResult;
3148
3149/// Verify an agent's DNS TXT record matches its public key hash.
3150///
3151/// Parses the agent JSON and looks up `_v1.agent.jacs.{domain}` to compare hashes.
3152/// Returns a structured result — never errors for DNS failures (those are `verified: false`).
3153pub fn verify_agent_dns(agent_json: &str, domain: &str) -> BindingResult<DnsVerificationResult> {
3154    jacs::dns::bootstrap::verify_agent_dns(agent_json, domain).map_err(|e| {
3155        BindingCoreError::invalid_argument(format!("DNS verification setup failed: {}", e))
3156    })
3157}
3158
3159// =============================================================================
3160// Re-exports for convenience
3161// =============================================================================
3162
3163pub use jacs;
3164
3165// =============================================================================
3166// Tests
3167// =============================================================================
3168
3169#[cfg(test)]
3170mod tests {
3171    use super::*;
3172    use std::path::PathBuf;
3173
3174    fn cross_language_fixtures_dir() -> Option<PathBuf> {
3175        let workspace = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
3176            .parent()?
3177            .to_path_buf();
3178        let dir = workspace.join("jacs/tests/fixtures/cross-language");
3179        if dir.exists() { Some(dir) } else { None }
3180    }
3181
3182    #[test]
3183    fn verify_standalone_invalid_json_returns_valid_false() {
3184        let result = verify_document_standalone("not json", Some("local"), None, None).unwrap();
3185        assert!(!result.valid);
3186        assert_eq!(result.signer_id, "");
3187    }
3188
3189    #[test]
3190    fn verify_standalone_tampered_document_returns_valid_false_with_signer_id() {
3191        let tampered = r#"{"jacsSignature":{"agentID":"golden-test-agent","agentVersion":"v1"},"jacsSha256":"x"}"#;
3192        let result = verify_document_standalone(tampered, Some("local"), None, None).unwrap();
3193        assert!(!result.valid);
3194        assert_eq!(result.signer_id, "golden-test-agent");
3195    }
3196
3197    #[test]
3198    fn verify_standalone_golden_invalid_signature_returns_valid_false() {
3199        let invalid_sig =
3200            std::fs::read_to_string("../jacs/tests/fixtures/golden/invalid_signature.json")
3201                .unwrap_or_else(|_| {
3202                    r#"{"jacsSignature":{"agentID":"golden-test-agent"},"jacsSha256":"x"}"#
3203                        .to_string()
3204                });
3205        let result = verify_document_standalone(
3206            &invalid_sig,
3207            Some("local"),
3208            Some("../jacs/tests/fixtures"),
3209            Some("../jacs/tests/fixtures/keys"),
3210        )
3211        .unwrap();
3212        assert!(!result.valid);
3213        assert_eq!(result.signer_id, "golden-test-agent");
3214    }
3215
3216    #[test]
3217    fn verify_standalone_nonexistent_key_directory_returns_valid_false() {
3218        let doc = r#"{"jacsSignature":{"agentID":"some-agent"},"jacsSha256":"x"}"#;
3219        let result = verify_document_standalone(
3220            doc,
3221            Some("local"),
3222            Some("/nonexistent_data"),
3223            Some("/nonexistent_keys"),
3224        )
3225        .unwrap();
3226        assert!(!result.valid);
3227        assert_eq!(result.signer_id, "some-agent");
3228    }
3229
3230    #[test]
3231    #[ignore = "pre-existing: cross-language fixture verification fails with relative parent paths"]
3232    fn verify_standalone_accepts_relative_parent_paths_from_subdir() {
3233        let Some(fixtures_dir) = cross_language_fixtures_dir() else {
3234            eprintln!("Skipping: cross-language fixtures directory not found");
3235            return;
3236        };
3237        let signed_path = fixtures_dir.join("python_ed25519_signed.json");
3238        if !signed_path.exists() {
3239            eprintln!(
3240                "Skipping: fixture '{}' not found",
3241                signed_path.to_string_lossy()
3242            );
3243            return;
3244        }
3245        let signed = std::fs::read_to_string(&signed_path).expect("read python fixture");
3246
3247        let workspace = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
3248            .parent()
3249            .expect("workspace root")
3250            .to_path_buf();
3251        let jacsnpm_dir = workspace.join("jacsnpm");
3252        if !jacsnpm_dir.exists() {
3253            eprintln!("Skipping: jacsnpm directory not found");
3254            return;
3255        }
3256
3257        struct CwdGuard(PathBuf);
3258        impl Drop for CwdGuard {
3259            fn drop(&mut self) {
3260                let _ = std::env::set_current_dir(&self.0);
3261            }
3262        }
3263
3264        let original_cwd = std::env::current_dir().expect("current dir");
3265        std::env::set_current_dir(&jacsnpm_dir).expect("chdir to jacsnpm");
3266        let _guard = CwdGuard(original_cwd);
3267
3268        let rel = "../jacs/tests/fixtures/cross-language";
3269        let result = verify_document_standalone(&signed, Some("local"), Some(rel), Some(rel))
3270            .expect("standalone verify should not error");
3271        assert!(result.valid, "relative parent-path fixture should verify");
3272    }
3273
3274    #[test]
3275    fn verify_standalone_accepts_absolute_fixture_paths() {
3276        let Some(fixtures_dir) = cross_language_fixtures_dir() else {
3277            eprintln!("Skipping: cross-language fixtures directory not found");
3278            return;
3279        };
3280        let signed_path = fixtures_dir.join("python_ed25519_signed.json");
3281        if !signed_path.exists() {
3282            eprintln!(
3283                "Skipping: fixture '{}' not found",
3284                signed_path.to_string_lossy()
3285            );
3286            return;
3287        }
3288        let signed = std::fs::read_to_string(&signed_path).expect("read python fixture");
3289        let fixtures_abs = fixtures_dir
3290            .canonicalize()
3291            .unwrap_or_else(|_| fixtures_dir.clone());
3292        let fixtures_abs_str = fixtures_abs.to_string_lossy().to_string();
3293
3294        let result = verify_document_standalone(
3295            &signed,
3296            Some("local"),
3297            Some(&fixtures_abs_str),
3298            Some(&fixtures_abs_str),
3299        )
3300        .expect("standalone verify should not error");
3301        assert!(result.valid, "absolute-path fixture should verify");
3302    }
3303
3304    #[test]
3305    fn verify_standalone_uses_key_directory_when_data_directory_missing() {
3306        let Some(fixtures_dir) = cross_language_fixtures_dir() else {
3307            eprintln!("Skipping: cross-language fixtures directory not found");
3308            return;
3309        };
3310        let signed_path = fixtures_dir.join("python_ed25519_signed.json");
3311        if !signed_path.exists() {
3312            eprintln!(
3313                "Skipping: fixture '{}' not found",
3314                signed_path.to_string_lossy()
3315            );
3316            return;
3317        }
3318        let signed = std::fs::read_to_string(&signed_path).expect("read python fixture");
3319        let fixtures_abs = fixtures_dir
3320            .canonicalize()
3321            .unwrap_or_else(|_| fixtures_dir.clone());
3322        let fixtures_abs_str = fixtures_abs.to_string_lossy().to_string();
3323
3324        let result =
3325            verify_document_standalone(&signed, Some("local"), None, Some(&fixtures_abs_str))
3326                .expect("standalone verify should not error");
3327        assert!(
3328            result.valid,
3329            "key_directory should be usable as standalone storage root when data_directory is omitted"
3330        );
3331    }
3332
3333    #[test]
3334    fn verify_standalone_ignores_polluting_env_overrides() {
3335        let Some(fixtures_dir) = cross_language_fixtures_dir() else {
3336            eprintln!("Skipping: cross-language fixtures directory not found");
3337            return;
3338        };
3339        let signed_path = fixtures_dir.join("python_ed25519_signed.json");
3340        if !signed_path.exists() {
3341            eprintln!(
3342                "Skipping: fixture '{}' not found",
3343                signed_path.to_string_lossy()
3344            );
3345            return;
3346        }
3347        let signed = std::fs::read_to_string(&signed_path).expect("read python fixture");
3348        let fixtures_abs = fixtures_dir
3349            .canonicalize()
3350            .unwrap_or_else(|_| fixtures_dir.clone());
3351        let fixtures_abs_str = fixtures_abs.to_string_lossy().to_string();
3352
3353        struct EnvRestore(Vec<(&'static str, Option<std::ffi::OsString>)>);
3354        impl Drop for EnvRestore {
3355            fn drop(&mut self) {
3356                for (k, v) in &self.0 {
3357                    if let Some(val) = v {
3358                        // SAFETY: test-only env restoration.
3359                        unsafe { std::env::set_var(k, val) }
3360                    } else {
3361                        // SAFETY: removing missing env vars is safe.
3362                        unsafe { std::env::remove_var(k) }
3363                    }
3364                }
3365            }
3366        }
3367
3368        let keys = [
3369            "JACS_DATA_DIRECTORY",
3370            "JACS_KEY_DIRECTORY",
3371            "JACS_DEFAULT_STORAGE",
3372            "JACS_KEY_RESOLUTION",
3373        ];
3374        let mut prev = Vec::new();
3375        for k in keys {
3376            prev.push((k, std::env::var_os(k)));
3377        }
3378        let _restore = EnvRestore(prev);
3379
3380        // Simulate pollution from earlier tests in the same process.
3381        // SAFETY: test-only env manipulation.
3382        unsafe {
3383            std::env::set_var("JACS_DATA_DIRECTORY", "/tmp/does-not-exist");
3384            std::env::set_var("JACS_KEY_DIRECTORY", "/tmp/does-not-exist");
3385            std::env::set_var("JACS_DEFAULT_STORAGE", "memory");
3386            std::env::set_var("JACS_KEY_RESOLUTION", "remote");
3387        }
3388
3389        let result = verify_document_standalone(
3390            &signed,
3391            Some("local"),
3392            Some(&fixtures_abs_str),
3393            Some(&fixtures_abs_str),
3394        )
3395        .expect("standalone verify should not error");
3396
3397        assert!(
3398            result.valid,
3399            "verification should ignore ambient JACS_* env pollution"
3400        );
3401    }
3402
3403    #[test]
3404    fn audit_default_returns_ok_json_has_risks_and_health_checks() {
3405        let json = audit(None, None).unwrap();
3406        let v: serde_json::Value = serde_json::from_str(&json).unwrap();
3407        assert!(v.get("risks").is_some(), "audit JSON should have risks");
3408        assert!(
3409            v.get("health_checks").is_some(),
3410            "audit JSON should have health_checks"
3411        );
3412    }
3413
3414    // =========================================================================
3415    // A2A Protocol Tests
3416    // =========================================================================
3417
3418    /// Helper: create an ephemeral AgentWrapper for A2A tests.
3419    fn ephemeral_wrapper() -> AgentWrapper {
3420        let wrapper = AgentWrapper::new();
3421        wrapper.ephemeral(Some("ed25519")).unwrap();
3422        wrapper
3423    }
3424
3425    #[cfg(feature = "a2a")]
3426    #[test]
3427    fn test_export_agent_card_returns_valid_json() {
3428        let wrapper = ephemeral_wrapper();
3429        let card_json = wrapper.export_agent_card().unwrap();
3430        let card: Value = serde_json::from_str(&card_json).unwrap();
3431        assert!(card.get("name").is_some());
3432        assert!(card.get("protocolVersions").is_some());
3433        assert_eq!(card["protocolVersions"][0], "0.4.0");
3434    }
3435
3436    #[cfg(feature = "a2a")]
3437    #[test]
3438    #[allow(deprecated)]
3439    fn test_wrap_and_verify_a2a_artifact() {
3440        let wrapper = ephemeral_wrapper();
3441        let artifact = r#"{"content": "hello A2A"}"#;
3442
3443        let wrapped = wrapper
3444            .wrap_a2a_artifact(artifact, "message", None)
3445            .unwrap();
3446        let wrapped_value: Value = serde_json::from_str(&wrapped).unwrap();
3447        assert!(wrapped_value.get("jacsId").is_some());
3448        assert_eq!(wrapped_value["jacsType"], "a2a-message");
3449
3450        let result_json = wrapper.verify_a2a_artifact(&wrapped).unwrap();
3451        let result: Value = serde_json::from_str(&result_json).unwrap();
3452        assert_eq!(result["valid"], true);
3453        assert_eq!(result["status"], "SelfSigned");
3454    }
3455
3456    #[cfg(feature = "a2a")]
3457    #[test]
3458    fn test_sign_artifact_alias_matches_wrap() {
3459        let wrapper = ephemeral_wrapper();
3460        let artifact = r#"{"data": 42}"#;
3461
3462        let signed = wrapper.sign_artifact(artifact, "artifact", None).unwrap();
3463        let value: Value = serde_json::from_str(&signed).unwrap();
3464        assert_eq!(value["jacsType"], "a2a-artifact");
3465
3466        let result_json = wrapper.verify_a2a_artifact(&signed).unwrap();
3467        let result: Value = serde_json::from_str(&result_json).unwrap();
3468        assert_eq!(result["valid"], true);
3469    }
3470
3471    #[cfg(feature = "a2a")]
3472    #[test]
3473    #[allow(deprecated)]
3474    fn test_wrap_a2a_artifact_with_parent_chain() {
3475        let wrapper = ephemeral_wrapper();
3476
3477        let first = wrapper
3478            .wrap_a2a_artifact(r#"{"step": 1}"#, "task", None)
3479            .unwrap();
3480        let parents = format!("[{}]", first);
3481        let second = wrapper
3482            .wrap_a2a_artifact(r#"{"step": 2}"#, "task", Some(&parents))
3483            .unwrap();
3484
3485        let second_value: Value = serde_json::from_str(&second).unwrap();
3486        let parent_sigs = second_value["jacsParentSignatures"].as_array().unwrap();
3487        assert_eq!(parent_sigs.len(), 1);
3488    }
3489
3490    #[cfg(feature = "a2a")]
3491    #[test]
3492    #[allow(deprecated)]
3493    fn test_wrap_a2a_artifact_invalid_json_error() {
3494        let wrapper = ephemeral_wrapper();
3495        let result = wrapper.wrap_a2a_artifact("not json", "artifact", None);
3496        assert!(result.is_err());
3497        assert_eq!(result.unwrap_err().kind, ErrorKind::InvalidArgument);
3498    }
3499
3500    #[cfg(feature = "a2a")]
3501    #[test]
3502    fn test_verify_a2a_artifact_invalid_json_error() {
3503        let wrapper = ephemeral_wrapper();
3504        let result = wrapper.verify_a2a_artifact("not json");
3505        assert!(result.is_err());
3506        assert_eq!(result.unwrap_err().kind, ErrorKind::InvalidArgument);
3507    }
3508
3509    #[cfg(feature = "a2a")]
3510    #[test]
3511    fn test_export_agent_card_unloaded_agent_error() {
3512        let wrapper = AgentWrapper::new();
3513        let result = wrapper.export_agent_card();
3514        assert!(result.is_err());
3515    }
3516
3517    // =========================================================================
3518    // Protocol Wrapper Tests
3519    // =========================================================================
3520
3521    /// Helper: create an ephemeral AgentWrapper for protocol tests.
3522    fn protocol_wrapper() -> AgentWrapper {
3523        let wrapper = AgentWrapper::new();
3524        wrapper.ephemeral(Some("ed25519")).unwrap();
3525        wrapper
3526    }
3527
3528    #[test]
3529    fn protocol_build_auth_header_starts_with_jacs() {
3530        let wrapper = protocol_wrapper();
3531        let header = wrapper
3532            .build_auth_header()
3533            .expect("build_auth_header failed");
3534        assert!(
3535            header.starts_with("JACS "),
3536            "Header must start with 'JACS ', got: {header}"
3537        );
3538    }
3539
3540    #[test]
3541    fn protocol_canonicalize_json_sorts_keys() {
3542        let wrapper = protocol_wrapper();
3543        let result = wrapper
3544            .canonicalize_json(r#"{"b":1,"a":2}"#)
3545            .expect("canonicalize_json failed");
3546        assert_eq!(result, r#"{"a":2,"b":1}"#);
3547    }
3548
3549    #[test]
3550    fn protocol_canonicalize_json_invalid_input() {
3551        let wrapper = protocol_wrapper();
3552        let result = wrapper.canonicalize_json("not json");
3553        assert!(result.is_err());
3554        assert_eq!(result.unwrap_err().kind, ErrorKind::SerializationFailed);
3555    }
3556
3557    #[test]
3558    fn protocol_sign_response_has_required_fields() {
3559        let wrapper = protocol_wrapper();
3560        let result = wrapper
3561            .sign_response(r#"{"answer": 42}"#)
3562            .expect("sign_response failed");
3563        let envelope: Value = serde_json::from_str(&result).expect("should be valid JSON");
3564        assert!(envelope.get("version").is_some(), "missing 'version'");
3565        assert!(
3566            envelope.get("jacsSignature").is_some(),
3567            "missing 'jacsSignature'"
3568        );
3569        assert_eq!(envelope["version"], "1.0.0");
3570    }
3571
3572    #[test]
3573    fn protocol_sign_response_invalid_payload() {
3574        let wrapper = protocol_wrapper();
3575        let result = wrapper.sign_response("not json");
3576        assert!(result.is_err());
3577        assert_eq!(result.unwrap_err().kind, ErrorKind::SerializationFailed);
3578    }
3579
3580    #[test]
3581    fn protocol_encode_verify_payload_round_trips() {
3582        let wrapper = protocol_wrapper();
3583        let original = r#"{"test":true}"#;
3584        let encoded = wrapper
3585            .encode_verify_payload(original)
3586            .expect("encode_verify_payload failed");
3587        assert!(!encoded.contains('+'), "URL-safe base64 must not contain +");
3588        assert!(!encoded.contains('/'), "URL-safe base64 must not contain /");
3589        assert!(
3590            !encoded.contains('='),
3591            "URL-safe base64 must not have padding"
3592        );
3593        let decoded = wrapper
3594            .decode_verify_payload(&encoded)
3595            .expect("decode_verify_payload failed");
3596        assert_eq!(decoded, original);
3597    }
3598
3599    #[test]
3600    fn protocol_extract_document_id_extracts_id() {
3601        let wrapper = protocol_wrapper();
3602        let id = wrapper
3603            .extract_document_id(r#"{"jacsDocumentId":"abc-123"}"#)
3604            .expect("extract_document_id failed");
3605        assert_eq!(id, "abc-123");
3606    }
3607
3608    #[test]
3609    fn protocol_extract_document_id_no_id_errors() {
3610        let wrapper = protocol_wrapper();
3611        let result = wrapper.extract_document_id(r#"{"name":"no-id"}"#);
3612        assert!(result.is_err());
3613    }
3614
3615    #[test]
3616    fn protocol_unwrap_signed_event_unknown_agent_unverified() {
3617        let wrapper = protocol_wrapper();
3618        let event = r#"{"data":{"result":"hello"},"jacsSignature":{"agentID":"unknown:v1","date":"2026-01-01T00:00:00Z","signature":"fakesig"}}"#;
3619        let keys = r#"{}"#;
3620        let result = wrapper
3621            .unwrap_signed_event(event, keys)
3622            .expect("unwrap_signed_event failed");
3623        let parsed: Value = serde_json::from_str(&result).expect("should be valid JSON");
3624        assert_eq!(parsed["verified"], false);
3625        assert_eq!(parsed["data"]["result"], "hello");
3626    }
3627
3628    #[test]
3629    fn protocol_unwrap_signed_event_legacy_payload() {
3630        let wrapper = protocol_wrapper();
3631        let event = r#"{"payload":{"status":"ok"}}"#;
3632        let keys = r#"{}"#;
3633        let result = wrapper
3634            .unwrap_signed_event(event, keys)
3635            .expect("unwrap_signed_event failed");
3636        let parsed: Value = serde_json::from_str(&result).expect("should be valid JSON");
3637        assert_eq!(parsed["verified"], false);
3638        assert_eq!(parsed["data"]["status"], "ok");
3639    }
3640
3641    #[test]
3642    fn protocol_unwrap_signed_event_plain_event() {
3643        let wrapper = protocol_wrapper();
3644        let event = r#"{"type":"heartbeat","ts":12345}"#;
3645        let keys = r#"{}"#;
3646        let result = wrapper
3647            .unwrap_signed_event(event, keys)
3648            .expect("unwrap_signed_event failed");
3649        let parsed: Value = serde_json::from_str(&result).expect("should be valid JSON");
3650        assert_eq!(parsed["verified"], false);
3651        assert_eq!(parsed["data"]["type"], "heartbeat");
3652    }
3653
3654    #[test]
3655    fn protocol_unwrap_signed_event_invalid_event_json() {
3656        let wrapper = protocol_wrapper();
3657        let result = wrapper.unwrap_signed_event("not json", "{}");
3658        assert!(result.is_err());
3659        assert_eq!(result.unwrap_err().kind, ErrorKind::SerializationFailed);
3660    }
3661
3662    #[test]
3663    fn protocol_unwrap_signed_event_invalid_keys_json() {
3664        let wrapper = protocol_wrapper();
3665        let result = wrapper.unwrap_signed_event(r#"{"type":"test"}"#, "not json");
3666        assert!(result.is_err());
3667        assert_eq!(result.unwrap_err().kind, ErrorKind::SerializationFailed);
3668    }
3669
3670    // =========================================================================
3671    // Attestation API Tests
3672    // =========================================================================
3673
3674    #[cfg(feature = "attestation")]
3675    mod attestation_tests {
3676        use super::*;
3677
3678        fn attestation_wrapper() -> AgentWrapper {
3679            let wrapper = AgentWrapper::new();
3680            wrapper.ephemeral(Some("ed25519")).unwrap();
3681            wrapper
3682        }
3683
3684        fn basic_attestation_params() -> String {
3685            json!({
3686                "subject": {
3687                    "type": "artifact",
3688                    "id": "test-artifact-001",
3689                    "digests": { "sha256": "abc123" }
3690                },
3691                "claims": [{
3692                    "name": "reviewed",
3693                    "value": true,
3694                    "confidence": 0.95,
3695                    "assuranceLevel": "verified"
3696                }]
3697            })
3698            .to_string()
3699        }
3700
3701        #[test]
3702        fn binding_create_attestation_json() {
3703            let wrapper = attestation_wrapper();
3704            let result = wrapper.create_attestation(&basic_attestation_params());
3705            assert!(
3706                result.is_ok(),
3707                "create_attestation should succeed: {:?}",
3708                result.err()
3709            );
3710
3711            let json_str = result.unwrap();
3712            let doc: Value = serde_json::from_str(&json_str).unwrap();
3713            assert!(
3714                doc.get("attestation").is_some(),
3715                "returned JSON should contain 'attestation' key"
3716            );
3717            assert!(
3718                doc.get("jacsSignature").is_some(),
3719                "returned JSON should be signed"
3720            );
3721        }
3722
3723        #[test]
3724        fn binding_verify_attestation_json() {
3725            let wrapper = attestation_wrapper();
3726            let att_json = wrapper
3727                .create_attestation(&basic_attestation_params())
3728                .unwrap();
3729            let doc: Value = serde_json::from_str(&att_json).unwrap();
3730            let key = format!(
3731                "{}:{}",
3732                doc["jacsId"].as_str().unwrap(),
3733                doc["jacsVersion"].as_str().unwrap()
3734            );
3735
3736            let result = wrapper.verify_attestation(&key);
3737            assert!(
3738                result.is_ok(),
3739                "verify_attestation should succeed: {:?}",
3740                result.err()
3741            );
3742
3743            let result_json = result.unwrap();
3744            let result_value: Value = serde_json::from_str(&result_json).unwrap();
3745            assert_eq!(
3746                result_value["valid"], true,
3747                "attestation should verify as valid"
3748            );
3749        }
3750
3751        #[test]
3752        fn binding_verify_attestation_full_json() {
3753            let wrapper = attestation_wrapper();
3754            let att_json = wrapper
3755                .create_attestation(&basic_attestation_params())
3756                .unwrap();
3757            let doc: Value = serde_json::from_str(&att_json).unwrap();
3758            let key = format!(
3759                "{}:{}",
3760                doc["jacsId"].as_str().unwrap(),
3761                doc["jacsVersion"].as_str().unwrap()
3762            );
3763
3764            let result = wrapper.verify_attestation_full(&key);
3765            assert!(
3766                result.is_ok(),
3767                "verify_attestation_full should succeed: {:?}",
3768                result.err()
3769            );
3770
3771            let result_json = result.unwrap();
3772            let result_value: Value = serde_json::from_str(&result_json).unwrap();
3773            assert_eq!(
3774                result_value["valid"], true,
3775                "full attestation should verify as valid"
3776            );
3777            assert!(
3778                result_value.get("evidence").is_some(),
3779                "full verification result should contain 'evidence' array"
3780            );
3781        }
3782
3783        #[test]
3784        fn binding_lift_to_attestation_json() {
3785            let wrapper = attestation_wrapper();
3786
3787            // Create a proper signed JACS document
3788            let doc_json = json!({"title": "Test Document", "content": "Some content"}).to_string();
3789            let signed = wrapper
3790                .create_document(&doc_json, None, None, true, None, None)
3791                .unwrap();
3792
3793            let claims_json = json!([{
3794                "name": "reviewed",
3795                "value": true
3796            }])
3797            .to_string();
3798
3799            let result = wrapper.lift_to_attestation(&signed, &claims_json);
3800            assert!(
3801                result.is_ok(),
3802                "lift_to_attestation should succeed: {:?}",
3803                result.err()
3804            );
3805
3806            let att_json = result.unwrap();
3807            let doc: Value = serde_json::from_str(&att_json).unwrap();
3808            assert!(
3809                doc.get("attestation").is_some(),
3810                "lifted result should contain 'attestation' key"
3811            );
3812            assert!(
3813                doc.get("jacsSignature").is_some(),
3814                "lifted result should be signed"
3815            );
3816        }
3817
3818        #[test]
3819        fn binding_create_attestation_error_on_bad_json() {
3820            let wrapper = attestation_wrapper();
3821            let result = wrapper.create_attestation("not valid json {{{");
3822            assert!(result.is_err(), "bad JSON should error");
3823            assert_eq!(
3824                result.unwrap_err().kind,
3825                ErrorKind::SerializationFailed,
3826                "should be SerializationFailed error"
3827            );
3828        }
3829
3830        #[test]
3831        fn binding_create_attestation_error_on_missing_fields() {
3832            let wrapper = attestation_wrapper();
3833            // Valid JSON but missing required 'subject' field
3834            let params = json!({
3835                "claims": [{"name": "test", "value": true}]
3836            })
3837            .to_string();
3838
3839            let result = wrapper.create_attestation(&params);
3840            assert!(result.is_err(), "missing subject should error");
3841            assert_eq!(
3842                result.unwrap_err().kind,
3843                ErrorKind::Validation,
3844                "should be Validation error"
3845            );
3846        }
3847
3848        #[test]
3849        fn binding_export_attestation_dsse() {
3850            let wrapper = attestation_wrapper();
3851            let att_json = wrapper
3852                .create_attestation(&basic_attestation_params())
3853                .unwrap();
3854
3855            let result = wrapper.export_attestation_dsse(&att_json);
3856            assert!(
3857                result.is_ok(),
3858                "export_attestation_dsse should succeed: {:?}",
3859                result.err()
3860            );
3861
3862            let dsse_json = result.unwrap();
3863            let envelope: Value = serde_json::from_str(&dsse_json).unwrap();
3864            assert_eq!(
3865                envelope["payloadType"].as_str().unwrap(),
3866                "application/vnd.in-toto+json"
3867            );
3868        }
3869    }
3870}