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