use base64::{Engine as _, engine::general_purpose};
use jacs::agent::agreement::Agreement;
use jacs::agent::boilerplate::BoilerPlate;
use jacs::agent::document::{DocumentTraits, JACSDocument};
use jacs::agent::payloads::PayloadTraits;
use jacs::agent::{
AGENT_AGREEMENT_FIELDNAME, AGENT_REGISTRATION_SIGNATURE_FIELDNAME, AGENT_SIGNATURE_FIELDNAME,
Agent,
};
use jacs::config::Config;
use jacs::crypt::KeyManager;
use jacs::crypt::hash::hash_string as jacs_hash_string;
use reqwest::blocking::Client as BlockingClient;
use reqwest::header::{ACCEPT, CONTENT_TYPE};
use reqwest::{StatusCode, Url};
use serde_json::{Value, json};
use std::collections::HashMap;
use std::fs;
use std::fs::OpenOptions;
use std::io::Write;
#[cfg(unix)]
use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
pub mod conversion;
pub mod doc_wrapper;
pub mod simple_wrapper;
pub use doc_wrapper::DocumentServiceWrapper;
pub use simple_wrapper::SimpleAgentWrapper;
pub use simple_wrapper::sign_message_json;
pub use simple_wrapper::verify_json;
#[derive(Debug)]
pub struct BindingCoreError {
pub message: String,
pub kind: ErrorKind,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ErrorKind {
LockFailed,
AgentLoad,
Validation,
SigningFailed,
VerificationFailed,
DocumentFailed,
AgreementFailed,
SerializationFailed,
InvalidArgument,
TrustFailed,
NetworkFailed,
KeyNotFound,
Generic,
}
impl BindingCoreError {
pub fn new(kind: ErrorKind, message: impl Into<String>) -> Self {
Self {
message: message.into(),
kind,
}
}
pub fn lock_failed(message: impl Into<String>) -> Self {
Self::new(ErrorKind::LockFailed, message)
}
pub fn agent_load(message: impl Into<String>) -> Self {
Self::new(ErrorKind::AgentLoad, message)
}
pub fn validation(message: impl Into<String>) -> Self {
Self::new(ErrorKind::Validation, message)
}
pub fn signing_failed(message: impl Into<String>) -> Self {
Self::new(ErrorKind::SigningFailed, message)
}
pub fn verification_failed(message: impl Into<String>) -> Self {
Self::new(ErrorKind::VerificationFailed, message)
}
pub fn document_failed(message: impl Into<String>) -> Self {
Self::new(ErrorKind::DocumentFailed, message)
}
pub fn agreement_failed(message: impl Into<String>) -> Self {
Self::new(ErrorKind::AgreementFailed, message)
}
pub fn serialization_failed(message: impl Into<String>) -> Self {
Self::new(ErrorKind::SerializationFailed, message)
}
pub fn invalid_argument(message: impl Into<String>) -> Self {
Self::new(ErrorKind::InvalidArgument, message)
}
pub fn trust_failed(message: impl Into<String>) -> Self {
Self::new(ErrorKind::TrustFailed, message)
}
pub fn network_failed(message: impl Into<String>) -> Self {
Self::new(ErrorKind::NetworkFailed, message)
}
pub fn key_not_found(message: impl Into<String>) -> Self {
Self::new(ErrorKind::KeyNotFound, message)
}
pub fn generic(message: impl Into<String>) -> Self {
Self::new(ErrorKind::Generic, message)
}
}
impl std::fmt::Display for BindingCoreError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.message)
}
}
impl std::error::Error for BindingCoreError {}
impl<T> From<PoisonError<T>> for BindingCoreError {
fn from(e: PoisonError<T>) -> Self {
Self::lock_failed(format!("Failed to acquire lock: {}", e))
}
}
pub type BindingResult<T> = Result<T, BindingCoreError>;
fn serialize_agent_info(info: &jacs::simple::AgentInfo) -> BindingResult<String> {
serde_json::to_string(info).map_err(|e| {
BindingCoreError::serialization_failed(format!("Failed to serialize AgentInfo: {}", e))
})
}
fn resolve_existing_config_path(config_path: &str) -> BindingResult<String> {
let requested = Path::new(config_path);
let resolved = if requested.is_absolute() {
requested.to_path_buf()
} else {
std::env::current_dir()
.map_err(|e| {
BindingCoreError::agent_load(format!(
"Failed to determine current working directory: {}",
e
))
})?
.join(requested)
};
if !resolved.exists() {
return Err(BindingCoreError::agent_load(format!(
"Config file not found: {}",
resolved.display()
)));
}
Ok(normalize_path(&resolved).to_string_lossy().into_owned())
}
fn normalize_path(path: &Path) -> PathBuf {
let mut normalized = PathBuf::new();
for component in path.components() {
match component {
std::path::Component::CurDir => {}
std::path::Component::ParentDir => {
normalized.pop();
}
other => normalized.push(other.as_os_str()),
}
}
normalized
}
fn resolve_path_from_cwd(path: &str) -> BindingResult<PathBuf> {
let requested = Path::new(path);
if requested.is_absolute() {
return Ok(normalize_path(requested));
}
Ok(normalize_path(
&std::env::current_dir()
.map_err(|e| {
BindingCoreError::agent_load(format!(
"Failed to determine current working directory: {}",
e
))
})?
.join(requested),
))
}
fn resolve_relative_to_config(config_path: &Path, candidate: &str) -> PathBuf {
let candidate_path = Path::new(candidate);
if candidate_path.is_absolute() {
return normalize_path(candidate_path);
}
let base_dir = config_path.parent().unwrap_or_else(|| Path::new("."));
normalize_path(&base_dir.join(candidate_path))
}
fn read_password_file(path: &Path) -> BindingResult<Option<String>> {
if !path.exists() {
return Ok(None);
}
#[cfg(unix)]
{
let metadata = fs::metadata(path).map_err(|e| {
BindingCoreError::generic(format!(
"Failed to inspect password file {}: {}",
path.display(),
e
))
})?;
let mode = metadata.permissions().mode() & 0o777;
if mode & 0o077 != 0 {
return Err(BindingCoreError::generic(format!(
"Password file {} has insecure permissions (mode {:04o}). \
File must not be group-readable or world-readable.",
path.display(),
mode
)));
}
if !metadata.is_file() {
return Err(BindingCoreError::generic(format!(
"Password file {} is not a regular file.",
path.display()
)));
}
}
let contents = fs::read_to_string(path).map_err(|e| {
BindingCoreError::generic(format!(
"Failed to read password file {}: {}",
path.display(),
e
))
})?;
let password = contents.trim_end_matches(|c| c == '\n' || c == '\r').trim();
if password.is_empty() {
return Ok(None);
}
Ok(Some(password.to_string()))
}
fn missing_password_message(error: &str) -> bool {
error.contains("No private key password available")
}
fn truthy_env_var(name: &str) -> bool {
std::env::var(name)
.ok()
.map(|value| {
let value = value.trim();
value.eq_ignore_ascii_case("true") || value == "1"
})
.unwrap_or(false)
}
const DEFAULT_NETWORK_TIMEOUT_MS: u64 = 10_000;
const DEFAULT_KEYS_BASE_URL: &str = "https://hai.ai";
fn build_blocking_json_client(timeout_ms: u64) -> BindingResult<BlockingClient> {
BlockingClient::builder()
.timeout(std::time::Duration::from_millis(timeout_ms.max(1)))
.build()
.map_err(|e| {
BindingCoreError::network_failed(format!("Failed to build HTTP client: {}", e))
})
}
fn is_loopback_host(host: &str) -> bool {
matches!(
host.trim().trim_matches(['[', ']']),
"localhost" | "127.0.0.1" | "::1"
)
}
fn validate_network_url(url: &Url, description: &str) -> BindingResult<()> {
match url.scheme() {
"https" => Ok(()),
"http" if url.host_str().is_some_and(is_loopback_host) => Ok(()),
"http" => Err(BindingCoreError::network_failed(format!(
"{} must use HTTPS (got '{}'). Only localhost URLs are allowed over HTTP for testing.",
description, url
))),
other => Err(BindingCoreError::invalid_argument(format!(
"{} must use http or https (got scheme '{}')",
description, other
))),
}
}
fn content_type_header(response: &reqwest::blocking::Response) -> String {
response
.headers()
.get(CONTENT_TYPE)
.and_then(|value| value.to_str().ok())
.unwrap_or("")
.to_string()
}
fn parse_json_object_body(
body: &str,
invalid_json_message: String,
non_object_message: String,
) -> BindingResult<String> {
let value: Value = serde_json::from_str(body)
.map_err(|e| BindingCoreError::validation(format!("{}: {}", invalid_json_message, e)))?;
if !value.is_object() {
return Err(BindingCoreError::validation(non_object_message));
}
serde_json::to_string(&value).map_err(|e| {
BindingCoreError::serialization_failed(format!("Failed to serialize JSON response: {}", e))
})
}
fn resolve_keys_base_url(override_base_url: Option<&str>) -> String {
if let Some(value) = override_base_url {
let trimmed = value.trim();
if !trimmed.is_empty() {
return trimmed.trim_end_matches('/').to_string();
}
}
if let Ok(value) = std::env::var("JACS_KEYS_BASE_URL") {
let trimmed = value.trim();
if !trimmed.is_empty() {
return trimmed.trim_end_matches('/').to_string();
}
}
if let Ok(value) = std::env::var("HAI_KEYS_BASE_URL") {
let trimmed = value.trim();
if !trimmed.is_empty() {
return trimmed.trim_end_matches('/').to_string();
}
}
DEFAULT_KEYS_BASE_URL.to_string()
}
fn normalize_public_key_hash(public_key_hash: &str) -> BindingResult<String> {
let trimmed = public_key_hash.trim();
if trimmed.is_empty() {
return Err(BindingCoreError::invalid_argument(
"public_key_hash cannot be empty",
));
}
if trimmed.starts_with("sha256:") {
Ok(trimmed.to_string())
} else {
Ok(format!("sha256:{}", trimmed))
}
}
fn decode_public_key_base64(public_key_b64: &str) -> BindingResult<Vec<u8>> {
for engine in [
&general_purpose::STANDARD,
&general_purpose::STANDARD_NO_PAD,
&general_purpose::URL_SAFE,
&general_purpose::URL_SAFE_NO_PAD,
] {
if let Ok(decoded) = engine.decode(public_key_b64) {
return Ok(decoded);
}
}
Err(BindingCoreError::invalid_argument(
"Public key must be valid base64 or base64url text.",
))
}
fn build_jwk_set_from_public_key_bytes(
public_key: &[u8],
key_algorithm: &str,
key_id: &str,
) -> BindingResult<Value> {
let normalized_algorithm = key_algorithm.trim().to_ascii_lowercase();
if normalized_algorithm.contains("ed25519")
|| (normalized_algorithm.is_empty() && public_key.len() == 32)
{
if public_key.len() != 32 {
return Err(BindingCoreError::invalid_argument(format!(
"Ed25519 public key must be 32 bytes, got {} bytes.",
public_key.len()
)));
}
return Ok(json!({
"keys": [{
"kty": "OKP",
"crv": "Ed25519",
"x": general_purpose::URL_SAFE_NO_PAD.encode(public_key),
"kid": key_id,
"use": "sig",
"alg": "EdDSA",
}]
}));
}
if normalized_algorithm.contains("rsa") || normalized_algorithm.is_empty() {
use rsa::traits::PublicKeyParts;
use rsa::{RsaPublicKey, pkcs1::DecodeRsaPublicKey, pkcs8::DecodePublicKey};
let rsa_key = if let Ok(key) = RsaPublicKey::from_pkcs1_der(public_key) {
key
} else if let Ok(key) = RsaPublicKey::from_public_key_der(public_key) {
key
} else if let Ok(pem) = std::str::from_utf8(public_key) {
match RsaPublicKey::from_public_key_pem(pem) {
Ok(key) => key,
Err(e) if normalized_algorithm.contains("rsa") => {
return Err(BindingCoreError::invalid_argument(format!(
"Failed to parse RSA public key for JWK export: {}",
e
)));
}
Err(_) => return Ok(json!({ "keys": [] })),
}
} else if normalized_algorithm.contains("rsa") {
return Err(BindingCoreError::invalid_argument(
"Failed to parse RSA public key for JWK export.",
));
} else {
return Ok(json!({ "keys": [] }));
};
return Ok(json!({
"keys": [{
"kty": "RSA",
"kid": key_id,
"alg": "RS256",
"use": "sig",
"n": general_purpose::URL_SAFE_NO_PAD.encode(rsa_key.n().to_bytes_be()),
"e": general_purpose::URL_SAFE_NO_PAD.encode(rsa_key.e().to_bytes_be()),
}]
}));
}
Ok(json!({ "keys": [] }))
}
fn resolve_password_context(
config_path: Option<&str>,
key_directory: Option<&str>,
) -> BindingResult<(PathBuf, Option<String>)> {
let mut agent_id = None;
if let Some(config_path) = config_path {
let resolved_config_path = resolve_path_from_cwd(config_path)?;
if resolved_config_path.exists() {
let config = Config::from_file(resolved_config_path.to_string_lossy().as_ref())
.map_err(|e| {
BindingCoreError::agent_load(format!(
"Failed to load config from {}: {}",
resolved_config_path.display(),
e
))
})?;
let configured_key_dir = config
.jacs_key_directory()
.as_deref()
.unwrap_or("./jacs_keys");
let resolved_key_dir =
resolve_relative_to_config(&resolved_config_path, configured_key_dir);
agent_id = config.jacs_agent_id_and_version().clone();
if let Some(key_directory) = key_directory {
return Ok((resolve_path_from_cwd(key_directory)?, agent_id));
}
return Ok((resolved_key_dir, agent_id));
}
if let Some(key_directory) = key_directory {
return Ok((resolve_path_from_cwd(key_directory)?, None));
}
return Ok((
resolve_relative_to_config(&resolved_config_path, "./jacs_keys"),
None,
));
}
if let Some(key_directory) = key_directory {
return Ok((resolve_path_from_cwd(key_directory)?, None));
}
Ok((resolve_path_from_cwd("./jacs_keys")?, agent_id))
}
fn generate_private_key_password_value() -> String {
use rand::Rng;
const UPPER: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ";
const LOWER: &[u8] = b"abcdefghijklmnopqrstuvwxyz";
const DIGITS: &[u8] = b"0123456789";
const SPECIAL: &[u8] = b"!@#$%^&*()-_=+";
const ALL: &[u8] =
b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()-_=+";
let mut rng = rand::rng();
let mut password = String::with_capacity(32);
password.push(UPPER[rng.random_range(0..UPPER.len())] as char);
password.push(LOWER[rng.random_range(0..LOWER.len())] as char);
password.push(DIGITS[rng.random_range(0..DIGITS.len())] as char);
password.push(SPECIAL[rng.random_range(0..SPECIAL.len())] as char);
for _ in 4..32 {
password.push(ALL[rng.random_range(0..ALL.len())] as char);
}
password
}
fn persist_password_file(key_directory: &Path, password: &str) -> BindingResult<()> {
fs::create_dir_all(key_directory).map_err(|e| {
BindingCoreError::generic(format!(
"Failed to create key directory {}: {}",
key_directory.display(),
e
))
})?;
let password_path = key_directory.join(".jacs_password");
let mut options = OpenOptions::new();
options.write(true).create_new(true);
#[cfg(unix)]
{
options.mode(0o600);
}
let mut file = options.open(&password_path).map_err(|e| {
BindingCoreError::generic(format!(
"Failed to create password file {} securely: {}",
password_path.display(),
e
))
})?;
file.write_all(password.as_bytes()).map_err(|e| {
BindingCoreError::generic(format!(
"Failed to write password file {}: {}",
password_path.display(),
e
))
})?;
file.sync_all().map_err(|e| {
BindingCoreError::generic(format!(
"Failed to flush password file {}: {}",
password_path.display(),
e
))
})?;
#[cfg(unix)]
{
let permissions = std::fs::Permissions::from_mode(0o600);
fs::set_permissions(&password_path, permissions).map_err(|e| {
BindingCoreError::generic(format!(
"Failed to set password file permissions on {}: {}",
password_path.display(),
e
))
})?;
}
Ok(())
}
fn is_editable_level(level: &str) -> bool {
matches!(level, "artifact" | "config")
}
fn normalize_agent_id_for_compare(agent_id: &str) -> &str {
agent_id.split(':').next().unwrap_or(agent_id)
}
fn extract_agreement_payload(value: &Value) -> Value {
if let Some(payload) = value.get("jacsDocument") {
return payload.clone();
}
if let Some(payload) = value.get("content") {
return payload.clone();
}
if let Some(obj) = value.as_object() {
let mut filtered = serde_json::Map::new();
for (k, v) in obj {
if !k.starts_with("jacs") && k != "$schema" {
filtered.insert(k.clone(), v.clone());
}
}
if !filtered.is_empty() {
return Value::Object(filtered);
}
}
Value::Null
}
fn create_editable_agreement_document(
agent: &mut Agent,
payload: Value,
) -> BindingResult<JACSDocument> {
let wrapped = json!({
"jacsType": "artifact",
"jacsLevel": "artifact",
"content": payload
});
agent
.create_document_and_load(&wrapped.to_string(), None, None)
.map_err(|e| {
BindingCoreError::document_failed(format!(
"Failed to create editable agreement document: {}",
e
))
})
}
fn ensure_editable_agreement_document(
agent: &mut Agent,
document_string: &str,
) -> BindingResult<JACSDocument> {
match agent.load_document(document_string) {
Ok(doc) => {
let level = doc
.value
.get("jacsLevel")
.and_then(|v| v.as_str())
.unwrap_or("");
if is_editable_level(level) {
Ok(doc)
} else {
let payload = extract_agreement_payload(doc.getvalue());
create_editable_agreement_document(agent, payload)
}
}
Err(load_err) => {
if let Ok(parsed) = serde_json::from_str::<Value>(document_string)
&& (parsed.get("jacsId").is_some() || parsed.get("jacsVersion").is_some())
{
return Err(BindingCoreError::document_failed(format!(
"Failed to load document: {}",
load_err
)));
}
let payload = serde_json::from_str::<Value>(document_string)
.unwrap_or_else(|_| Value::String(document_string.to_string()));
create_editable_agreement_document(agent, payload)
}
}
}
#[derive(Clone)]
pub struct AgentWrapper {
inner: Arc<Mutex<Agent>>,
private_key_password: Arc<Mutex<Option<String>>>,
}
impl Default for AgentWrapper {
fn default() -> Self {
Self::new()
}
}
impl AgentWrapper {
pub fn new() -> Self {
Self {
inner: Arc::new(Mutex::new(jacs::get_empty_agent())),
private_key_password: Arc::new(Mutex::new(None)),
}
}
pub fn from_inner(inner: Arc<Mutex<Agent>>) -> Self {
Self {
inner,
private_key_password: Arc::new(Mutex::new(None)),
}
}
pub fn inner_arc(&self) -> Arc<Mutex<Agent>> {
Arc::clone(&self.inner)
}
fn lock(&self) -> BindingResult<MutexGuard<'_, Agent>> {
self.inner.lock().map_err(BindingCoreError::from)
}
fn configured_private_key_password(&self) -> BindingResult<Option<String>> {
self.private_key_password
.lock()
.map_err(BindingCoreError::from)
.map(|password| password.clone())
}
fn with_private_key_password<T>(
&self,
operation: impl FnOnce() -> BindingResult<T>,
) -> BindingResult<T> {
{
let password = self.configured_private_key_password()?;
let mut agent = self.lock()?;
agent.set_password(password);
}
operation()
}
pub fn set_private_key_password(&self, password: Option<String>) -> BindingResult<()> {
let mut slot = self
.private_key_password
.lock()
.map_err(BindingCoreError::from)?;
*slot = password.and_then(|value| if value.is_empty() { None } else { Some(value) });
Ok(())
}
pub fn load(&self, config_path: String) -> BindingResult<String> {
let password = self.configured_private_key_password()?;
let new_agent = self.load_agent_from_config(&config_path, true, password.as_deref())?;
*self.lock()? = new_agent;
Ok("Agent loaded".to_string())
}
pub fn load_file_only(&self, config_path: String) -> BindingResult<String> {
let new_agent = self.load_agent_from_config(&config_path, false, None)?;
*self.lock()? = new_agent;
Ok("Agent loaded (file-only)".to_string())
}
pub fn load_with_info(&self, config_path: String) -> BindingResult<String> {
let resolved_config_path = resolve_existing_config_path(&config_path)?;
let password = self.configured_private_key_password()?;
let new_agent =
self.load_agent_from_config(&resolved_config_path, true, password.as_deref())?;
let info = jacs::simple::build_loaded_agent_info(&new_agent, &resolved_config_path)
.map_err(|e| BindingCoreError::agent_load(format!("Failed to load agent: {}", e)))?;
*self.lock()? = new_agent;
serialize_agent_info(&info)
}
fn load_agent_from_config(
&self,
config_path: &str,
apply_env: bool,
password: Option<&str>,
) -> BindingResult<Agent> {
let mut config = Config::from_file(config_path)
.map_err(|e| BindingCoreError::agent_load(format!("Failed to load config: {}", e)))?;
if apply_env {
config.apply_env_overrides();
}
Agent::from_config(config, password)
.map_err(|e| BindingCoreError::agent_load(format!("Failed to load agent: {}", e)))
}
pub fn set_storage_root(&self, root: std::path::PathBuf) -> BindingResult<()> {
let mut agent = self.lock()?;
agent
.set_storage_root(root)
.map_err(|e| BindingCoreError::generic(format!("Failed to set storage root: {}", e)))?;
Ok(())
}
pub fn sign_agent(
&self,
agent_string: &str,
public_key: Vec<u8>,
public_key_enc_type: String,
) -> BindingResult<String> {
self.with_private_key_password(|| {
let mut agent = self.lock()?;
let mut external_agent: Value = agent.validate_agent(agent_string).map_err(|e| {
BindingCoreError::validation(format!("Agent validation failed: {}", e))
})?;
agent
.signature_verification_procedure(
&external_agent,
None,
&AGENT_SIGNATURE_FIELDNAME.to_string(),
public_key,
Some(public_key_enc_type),
None,
None,
)
.map_err(|e| {
BindingCoreError::verification_failed(format!(
"Signature verification failed: {}",
e
))
})?;
let registration_signature = agent
.signing_procedure(
&external_agent,
None,
&AGENT_REGISTRATION_SIGNATURE_FIELDNAME.to_string(),
)
.map_err(|e| {
BindingCoreError::signing_failed(format!("Signing procedure failed: {}", e))
})?;
external_agent[AGENT_REGISTRATION_SIGNATURE_FIELDNAME] = registration_signature;
Ok(external_agent.to_string())
})
}
pub fn verify_string(
&self,
data: &str,
signature_base64: &str,
public_key: Vec<u8>,
public_key_enc_type: String,
) -> BindingResult<bool> {
let agent = self.lock()?;
if data.is_empty()
|| signature_base64.is_empty()
|| public_key.is_empty()
|| public_key_enc_type.is_empty()
{
return Err(BindingCoreError::invalid_argument(format!(
"One parameter is empty: data={}, signature_base64={}, public_key_enc_type={}",
data.is_empty(),
signature_base64.is_empty(),
public_key_enc_type
)));
}
agent
.verify_string(
&data.to_string(),
&signature_base64.to_string(),
public_key,
Some(public_key_enc_type),
)
.map_err(|e| {
BindingCoreError::verification_failed(format!(
"Signature verification failed: {}",
e
))
})?;
Ok(true)
}
pub fn sign_string(&self, data: &str) -> BindingResult<String> {
self.with_private_key_password(|| {
let mut agent = self.lock()?;
agent.sign_string(&data.to_string()).map_err(|e| {
BindingCoreError::signing_failed(format!("Failed to sign string: {}", e))
})
})
}
pub fn sign_batch(&self, messages: Vec<String>) -> BindingResult<Vec<String>> {
self.with_private_key_password(|| {
let mut agent = self.lock()?;
let refs: Vec<&str> = messages.iter().map(|s| s.as_str()).collect();
agent
.sign_batch(&refs)
.map_err(|e| BindingCoreError::signing_failed(format!("Batch sign failed: {}", e)))
})
}
pub fn verify_agent(&self, agentfile: Option<String>) -> BindingResult<bool> {
self.with_private_key_password(|| {
let mut agent = self.lock()?;
if let Some(file) = agentfile {
let loaded_agent = jacs::load_agent(Some(file)).map_err(|e| {
BindingCoreError::agent_load(format!("Failed to load agent: {}", e))
})?;
*agent = loaded_agent;
}
agent.verify_self_signature().map_err(|e| {
BindingCoreError::verification_failed(format!(
"Failed to verify agent signature: {}",
e
))
})?;
agent.verify_self_hash().map_err(|e| {
BindingCoreError::verification_failed(format!("Failed to verify agent hash: {}", e))
})?;
Ok(true)
})
}
pub fn update_agent(&self, new_agent_string: &str) -> BindingResult<String> {
self.with_private_key_password(|| {
let mut agent = self.lock()?;
agent
.update_self(new_agent_string)
.map_err(|e| BindingCoreError::agent_load(format!("Failed to update agent: {}", e)))
})
}
pub fn verify_document(&self, document_string: &str) -> BindingResult<bool> {
let mut agent = self.lock()?;
let doc = agent.load_document(document_string).map_err(|e| {
BindingCoreError::document_failed(format!("Failed to load document: {}", e))
})?;
let document_key = doc.getkey();
let value = doc.getvalue();
agent.verify_hash(value).map_err(|e| {
BindingCoreError::verification_failed(format!("Failed to verify document hash: {}", e))
})?;
if agent
.verify_document_signature(&document_key, None, None, None, None)
.is_err()
{
agent
.verify_external_document_signature(&document_key)
.map_err(|e| {
BindingCoreError::verification_failed(format!(
"Failed to verify document signature: {}",
e
))
})?;
}
Ok(true)
}
pub fn update_document(
&self,
document_key: &str,
new_document_string: &str,
attachments: Option<Vec<String>>,
embed: Option<bool>,
) -> BindingResult<String> {
self.with_private_key_password(|| {
let mut agent = self.lock()?;
let doc = agent
.update_document(document_key, new_document_string, attachments, embed)
.map_err(|e| {
BindingCoreError::document_failed(format!("Failed to update document: {}", e))
})?;
Ok(doc.to_string())
})
}
pub fn verify_signature(
&self,
document_string: &str,
signature_field: Option<String>,
) -> BindingResult<bool> {
let mut agent = self.lock()?;
let doc = agent.load_document(document_string).map_err(|e| {
BindingCoreError::document_failed(format!("Failed to load document: {}", e))
})?;
let document_key = doc.getkey();
let sig_field_ref = signature_field.as_ref();
agent
.verify_document_signature(
&document_key,
sig_field_ref.map(|s| s.as_str()),
None,
None,
None,
)
.map_err(|e| {
BindingCoreError::verification_failed(format!("Failed to verify signature: {}", e))
})?;
Ok(true)
}
pub fn create_agreement(
&self,
document_string: &str,
agentids: Vec<String>,
question: Option<String>,
context: Option<String>,
agreement_fieldname: Option<String>,
) -> BindingResult<String> {
self.create_agreement_with_options(
document_string,
agentids,
question,
context,
agreement_fieldname,
None,
None,
None,
None,
)
}
pub fn create_agreement_with_options(
&self,
document_string: &str,
agentids: Vec<String>,
question: Option<String>,
context: Option<String>,
agreement_fieldname: Option<String>,
timeout: Option<String>,
quorum: Option<u32>,
required_algorithms: Option<Vec<String>>,
minimum_strength: Option<String>,
) -> BindingResult<String> {
use jacs::agent::agreement::{Agreement, AgreementOptions};
self.with_private_key_password(|| {
let mut agent = self.lock()?;
let base_doc = ensure_editable_agreement_document(&mut agent, document_string)?;
let document_key = base_doc.getkey();
let options = AgreementOptions {
timeout,
quorum,
required_algorithms,
minimum_strength,
};
let agreement_doc = agent
.create_agreement_with_options(
&document_key,
agentids.as_slice(),
question.as_deref(),
context.as_deref(),
agreement_fieldname,
&options,
)
.map_err(|e| {
BindingCoreError::agreement_failed(format!("Failed to create agreement: {}", e))
})?;
Ok(agreement_doc.value.to_string())
})
}
pub fn sign_agreement(
&self,
document_string: &str,
agreement_fieldname: Option<String>,
) -> BindingResult<String> {
self.with_private_key_password(|| {
let mut agent = self.lock()?;
let doc = agent.load_document(document_string).map_err(|e| {
BindingCoreError::document_failed(format!("Failed to load document: {}", e))
})?;
let document_key = doc.getkey();
let signed_doc = agent
.sign_agreement(&document_key, agreement_fieldname)
.map_err(|e| {
BindingCoreError::agreement_failed(format!("Failed to sign agreement: {}", e))
})?;
Ok(signed_doc.value.to_string())
})
}
pub fn create_document(
&self,
document_string: &str,
custom_schema: Option<String>,
outputfilename: Option<String>,
no_save: bool,
attachments: Option<&str>,
embed: Option<bool>,
) -> BindingResult<String> {
self.with_private_key_password(|| {
let mut agent = self.lock()?;
jacs::shared::document_create(
&mut agent,
document_string,
custom_schema,
outputfilename,
no_save,
attachments,
embed,
)
.map_err(|e| {
BindingCoreError::document_failed(format!("Failed to create document: {}", e))
})
})
}
pub fn save_signed_document(
&self,
document_string: &str,
outputfilename: Option<String>,
export_embedded: Option<bool>,
extract_only: Option<bool>,
) -> BindingResult<String> {
let mut agent = self.lock()?;
let doc = agent.load_document(document_string).map_err(|e| {
BindingCoreError::document_failed(format!("Failed to load signed document: {}", e))
})?;
let document_key = doc.getkey();
agent
.save_document(&document_key, outputfilename, export_embedded, extract_only)
.map_err(|e| {
BindingCoreError::document_failed(format!(
"Failed to persist signed document '{}': {}",
document_key, e
))
})?;
Ok(document_key)
}
pub fn list_document_keys(&self) -> BindingResult<Vec<String>> {
let mut agent = self.lock()?;
Ok(agent.get_document_keys())
}
pub fn check_agreement(
&self,
document_string: &str,
agreement_fieldname: Option<String>,
) -> BindingResult<String> {
let mut agent = self.lock()?;
let doc = agent.load_document(document_string).map_err(|e| {
BindingCoreError::document_failed(format!("Failed to load document: {}", e))
})?;
let document_key = doc.getkey();
let agreement_fieldname_key = agreement_fieldname
.clone()
.unwrap_or_else(|| AGENT_AGREEMENT_FIELDNAME.to_string());
agent
.check_agreement(&document_key, Some(agreement_fieldname_key.clone()))
.map_err(|e| {
BindingCoreError::agreement_failed(format!("Failed to check agreement: {}", e))
})?;
let requested = doc
.agreement_requested_agents(Some(agreement_fieldname_key.clone()))
.map_err(|e| {
BindingCoreError::agreement_failed(format!(
"Failed to read requested signers: {}",
e
))
})?;
let pending = doc
.agreement_unsigned_agents(Some(agreement_fieldname_key.clone()))
.map_err(|e| {
BindingCoreError::agreement_failed(format!("Failed to read pending signers: {}", e))
})?;
let signatures = doc
.value
.get(&agreement_fieldname_key)
.and_then(|agreement| agreement.get("signatures"))
.and_then(|v| v.as_array())
.cloned()
.unwrap_or_default();
let mut signed_at_by_agent: HashMap<String, String> = HashMap::new();
for signature in signatures {
if let Some(agent_id) = signature.get("agentID").and_then(|v| v.as_str()) {
let normalized = normalize_agent_id_for_compare(agent_id).to_string();
let signed_at = signature
.get("date")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
signed_at_by_agent.insert(normalized, signed_at);
}
}
let signers = requested
.iter()
.map(|agent_id| {
let normalized = normalize_agent_id_for_compare(agent_id).to_string();
let signed_at = signed_at_by_agent
.get(&normalized)
.filter(|ts| !ts.is_empty())
.cloned();
let signed = signed_at.is_some();
let mut signer = json!({
"agentId": agent_id,
"agent_id": agent_id,
"signed": signed
});
if let Some(ts) = signed_at {
signer["signedAt"] = json!(ts.clone());
signer["signed_at"] = json!(ts);
}
signer
})
.collect::<Vec<Value>>();
let result = json!({
"complete": pending.is_empty(),
"signers": signers,
"pending": pending
});
Ok(result.to_string())
}
pub fn sign_request(&self, payload_value: Value) -> BindingResult<String> {
self.with_private_key_password(|| {
let mut agent = self.lock()?;
let wrapper_value = serde_json::json!({
"jacs_payload": payload_value
});
let wrapper_string = serde_json::to_string(&wrapper_value).map_err(|e| {
BindingCoreError::serialization_failed(format!(
"Failed to serialize wrapper JSON: {}",
e
))
})?;
jacs::shared::document_create(
&mut agent,
&wrapper_string,
None,
None,
true, None,
Some(false),
)
.map_err(|e| {
BindingCoreError::document_failed(format!("Failed to create document: {}", e))
})
})
}
pub fn verify_response(&self, document_string: String) -> BindingResult<Value> {
let mut agent = self.lock()?;
agent
.verify_payload(document_string, None)
.map_err(|e| BindingCoreError::verification_failed(e.to_string()))
}
pub fn verify_response_with_agent_id(
&self,
document_string: String,
) -> BindingResult<(Value, String)> {
let mut agent = self.lock()?;
agent
.verify_payload_with_agent_id(document_string, None)
.map_err(|e| BindingCoreError::verification_failed(e.to_string()))
}
pub fn verify_document_by_id(&self, document_id: &str) -> BindingResult<bool> {
use jacs::storage::StorageDocumentTraits;
if !document_id.contains(':') {
return Err(BindingCoreError::invalid_argument(format!(
"Document ID must be in 'uuid:version' format, got '{}'. \
Use verify_document() with the full JSON string instead.",
document_id
)));
}
let storage = jacs::storage::MultiStorage::default_new().map_err(|e| {
BindingCoreError::generic(format!("Failed to initialize storage: {}", e))
})?;
let doc = storage.get_document(document_id).map_err(|e| {
BindingCoreError::document_failed(format!(
"Failed to load document '{}' from storage: {}",
document_id, e
))
})?;
let doc_str = serde_json::to_string(&doc.value).map_err(|e| {
BindingCoreError::serialization_failed(format!(
"Failed to serialize document '{}': {}",
document_id, e
))
})?;
self.verify_document(&doc_str)
}
pub fn get_document_by_id(&self, document_id: &str) -> BindingResult<String> {
if !document_id.contains(':') {
return Err(BindingCoreError::invalid_argument(format!(
"Document ID must be in 'uuid:version' format, got '{}'.",
document_id
)));
}
let agent = self.lock()?;
let doc = agent.get_document(document_id).map_err(|e| {
BindingCoreError::document_failed(format!(
"Failed to load document '{}' from storage: {}",
document_id, e
))
})?;
serde_json::to_string(&doc.value).map_err(|e| {
BindingCoreError::serialization_failed(format!(
"Failed to serialize document '{}': {}",
document_id, e
))
})
}
pub fn get_agent_id(&self) -> BindingResult<String> {
let agent = self.lock()?;
let value = agent
.get_value()
.ok_or_else(|| BindingCoreError::agent_load("Agent not loaded. Call load() first."))?;
value
.get("jacsId")
.and_then(|v| v.as_str())
.map(str::to_string)
.filter(|id| !id.is_empty())
.ok_or_else(|| {
BindingCoreError::agent_load(
"Agent not loaded or has no jacsId. Call load() first.",
)
})
}
pub fn reencrypt_key(&self, old_password: &str, new_password: &str) -> BindingResult<()> {
use jacs::crypt::aes_encrypt::reencrypt_private_key;
let agent = self.lock()?;
let key_path = if let Some(config) = &agent.config {
let key_dir = config
.jacs_key_directory()
.as_deref()
.unwrap_or("./jacs_keys");
let key_file = config
.jacs_agent_private_key_filename()
.as_deref()
.unwrap_or("jacs.private.pem.enc");
format!("{}/{}", key_dir, key_file)
} else {
"./jacs_keys/jacs.private.pem.enc".to_string()
};
drop(agent);
let encrypted_data = std::fs::read(&key_path).map_err(|e| {
BindingCoreError::generic(format!(
"Failed to read private key file '{}': {}",
key_path, e
))
})?;
let re_encrypted = reencrypt_private_key(&encrypted_data, old_password, new_password)
.map_err(|e| BindingCoreError::generic(format!("Re-encryption failed: {}", e)))?;
std::fs::write(&key_path, &re_encrypted).map_err(|e| {
BindingCoreError::generic(format!(
"Failed to write re-encrypted key to '{}': {}",
key_path, e
))
})?;
Ok(())
}
pub fn rotate_keys(&self, algorithm: Option<&str>) -> BindingResult<String> {
let config_path = {
let agent = self.lock()?;
agent
.config
.as_ref()
.and_then(|c| c.config_dir())
.map(|dir| dir.join("jacs.config.json").display().to_string())
};
let result = jacs::simple::advanced::rotate_with_mutex(
&self.inner,
config_path.as_deref(),
algorithm,
)
.map_err(|e| BindingCoreError::generic(format!("Key rotation failed: {}", e)))?;
serde_json::to_string(&result).map_err(|e| {
BindingCoreError::serialization_failed(format!(
"Failed to serialize rotation result: {}",
e
))
})
}
pub fn ephemeral(&self, algorithm: Option<&str>) -> BindingResult<String> {
let algo = match algorithm.unwrap_or("pq2025") {
"ed25519" => "ring-Ed25519",
"rsa-pss" => "RSA-PSS",
"pq2025" => "pq2025",
other => other,
};
let mut agent = Agent::ephemeral(algo).map_err(|e| {
BindingCoreError::agent_load(format!("Failed to create ephemeral agent: {}", e))
})?;
let template = jacs::create_minimal_blank_agent("ai".to_string(), None, None, None)
.map_err(|e| {
BindingCoreError::agent_load(format!(
"Failed to create minimal agent template: {}",
e
))
})?;
let mut agent_json: Value = serde_json::from_str(&template).map_err(|e| {
BindingCoreError::serialization_failed(format!(
"Failed to parse agent template JSON: {}",
e
))
})?;
if let Some(obj) = agent_json.as_object_mut() {
obj.insert("name".to_string(), json!("ephemeral"));
obj.insert("description".to_string(), json!("Ephemeral JACS agent"));
}
let instance = agent
.create_agent_and_load(&agent_json.to_string(), true, Some(algo))
.map_err(|e| {
BindingCoreError::agent_load(format!("Failed to initialize ephemeral agent: {}", e))
})?;
let agent_id = instance["jacsId"].as_str().unwrap_or("").to_string();
let version = instance["jacsVersion"].as_str().unwrap_or("").to_string();
let mut inner = self.lock()?;
*inner = agent;
let info = json!({
"agent_id": agent_id,
"name": "ephemeral",
"version": version,
"algorithm": algo,
});
serde_json::to_string_pretty(&info).map_err(|e| {
BindingCoreError::serialization_failed(format!(
"Failed to serialize ephemeral agent info: {}",
e
))
})
}
pub fn diagnostics(&self) -> String {
let mut info = jacs::simple::diagnostics();
if let Ok(agent) = self.inner.lock() {
if agent.ready() {
info["agent_loaded"] = json!(true);
if let Some(value) = agent.get_value() {
info["agent_id"] = json!(value.get("jacsId").and_then(|v| v.as_str()));
info["agent_version"] =
json!(value.get("jacsVersion").and_then(|v| v.as_str()));
}
}
if let Some(config) = &agent.config {
if let Some(dir) = config.jacs_data_directory().as_ref() {
info["data_directory"] = json!(dir);
}
if let Some(dir) = config.jacs_key_directory().as_ref() {
info["key_directory"] = json!(dir);
}
if let Some(storage) = config.jacs_default_storage().as_ref() {
info["default_storage"] = json!(storage);
}
if let Some(algo) = config.jacs_agent_key_algorithm().as_ref() {
info["key_algorithm"] = json!(algo);
}
}
}
serde_json::to_string_pretty(&info).unwrap_or_default()
}
pub fn get_setup_instructions(&self, domain: &str, ttl: u32) -> BindingResult<String> {
use jacs::agent::boilerplate::BoilerPlate;
use jacs::dns::bootstrap::{
DigestEncoding, build_dns_record, dnssec_guidance, emit_azure_cli,
emit_cloudflare_curl, emit_gcloud_dns, emit_plain_bind, emit_route53_change_batch,
tld_requirement_text,
};
let agent = self.lock()?;
let agent_value = agent.get_value().cloned().unwrap_or(json!({}));
let agent_id = agent_value
.get("jacsId")
.and_then(|v| v.as_str())
.unwrap_or("");
if agent_id.is_empty() {
return Err(BindingCoreError::agent_load(
"Agent not loaded or has no jacsId. Call load() first.",
));
}
let pk = agent
.get_public_key()
.map_err(|e| BindingCoreError::generic(format!("Failed to get public key: {}", e)))?;
let digest = jacs::dns::bootstrap::pubkey_digest_b64(&pk);
let rr = build_dns_record(domain, ttl, agent_id, &digest, DigestEncoding::Base64);
let dns_record_bind = emit_plain_bind(&rr);
let dns_owner = rr.owner.clone();
let dns_record_value = rr.txt.clone();
let mut provider_commands = std::collections::HashMap::new();
provider_commands.insert("bind".to_string(), dns_record_bind.clone());
provider_commands.insert("route53".to_string(), emit_route53_change_batch(&rr));
provider_commands.insert("gcloud".to_string(), emit_gcloud_dns(&rr, "YOUR_ZONE_NAME"));
provider_commands.insert(
"azure".to_string(),
emit_azure_cli(&rr, "YOUR_RG", domain, "_v1.agent.jacs"),
);
provider_commands.insert(
"cloudflare".to_string(),
emit_cloudflare_curl(&rr, "YOUR_ZONE_ID"),
);
let mut dnssec_instructions = std::collections::HashMap::new();
for name in &["aws", "cloudflare", "azure", "gcloud"] {
dnssec_instructions.insert(name.to_string(), dnssec_guidance(name).to_string());
}
let tld_requirement = tld_requirement_text().to_string();
let well_known = json!({
"jacs_agent_id": agent_id,
"jacs_public_key_hash": digest,
"jacs_dns_record": dns_owner,
});
let well_known_json = serde_json::to_string_pretty(&well_known).unwrap_or_default();
let summary = format!(
"Setup instructions for agent {agent_id} on domain {domain}:\n\
\n\
1. DNS: Publish the following TXT record:\n\
{bind}\n\
\n\
2. DNSSEC: {dnssec}\n\
\n\
3. Domain requirement: {tld}\n\
\n\
4. .well-known: Serve the well-known JSON at /.well-known/jacs-agent.json",
agent_id = agent_id,
domain = domain,
bind = dns_record_bind,
dnssec = dnssec_guidance("aws"),
tld = tld_requirement,
);
let result = json!({
"dns_record_bind": dns_record_bind,
"dns_record_value": dns_record_value,
"dns_owner": dns_owner,
"provider_commands": provider_commands,
"dnssec_instructions": dnssec_instructions,
"tld_requirement": tld_requirement,
"well_known_json": well_known_json,
"summary": summary,
});
serde_json::to_string_pretty(&result).map_err(|e| {
BindingCoreError::serialization_failed(format!(
"Failed to serialize setup instructions: {}",
e
))
})
}
pub fn export_agent(&self) -> BindingResult<String> {
let agent = self.lock()?;
let value = agent
.get_value()
.cloned()
.ok_or_else(|| BindingCoreError::agent_load("Agent not loaded. Call load() first."))?;
serde_json::to_string_pretty(&value).map_err(|e| {
BindingCoreError::serialization_failed(format!(
"Failed to serialize agent document: {}",
e
))
})
}
pub fn get_public_key_pem(&self) -> BindingResult<String> {
let agent = self.lock()?;
let public_key = BoilerPlate::get_public_key(&*agent)
.map_err(|e| BindingCoreError::generic(format!("Failed to get public key: {}", e)))?;
Ok(jacs::crypt::normalize_public_key_pem(&public_key))
}
pub fn get_agent_json(&self) -> BindingResult<String> {
self.export_agent()
}
}
#[cfg(feature = "a2a")]
impl AgentWrapper {
pub fn export_agent_card(&self) -> BindingResult<String> {
let agent = self.lock()?;
let card = jacs::a2a::agent_card::export_agent_card(&agent).map_err(|e| {
BindingCoreError::generic(format!("Failed to export agent card: {}", e))
})?;
serde_json::to_string_pretty(&card).map_err(|e| {
BindingCoreError::serialization_failed(format!("Failed to serialize agent card: {}", e))
})
}
pub fn generate_well_known_documents(
&self,
a2a_algorithm: Option<&str>,
) -> BindingResult<String> {
let agent = self.lock()?;
let card = jacs::a2a::agent_card::export_agent_card(&agent).map_err(|e| {
BindingCoreError::generic(format!("Failed to export agent card: {}", e))
})?;
let a2a_alg = a2a_algorithm.unwrap_or("ring-Ed25519");
let dual_keys = jacs::a2a::keys::create_jwk_keys(None, Some(a2a_alg)).map_err(|e| {
BindingCoreError::generic(format!("Failed to generate A2A keys: {}", e))
})?;
let agent_id = agent
.get_id()
.map_err(|e| BindingCoreError::generic(format!("Failed to get agent ID: {}", e)))?;
let jws = jacs::a2a::extension::sign_agent_card_jws(
&card,
&dual_keys.a2a_private_key,
&dual_keys.a2a_algorithm,
&agent_id,
)
.map_err(|e| BindingCoreError::generic(format!("Failed to sign Agent Card: {}", e)))?;
let documents = jacs::a2a::extension::generate_well_known_documents(
&agent,
&card,
&dual_keys.a2a_public_key,
&dual_keys.a2a_algorithm,
&jws,
)
.map_err(|e| {
BindingCoreError::generic(format!("Failed to generate well-known documents: {}", e))
})?;
let pairs: Vec<Value> = documents
.into_iter()
.map(|(path, doc)| serde_json::json!({ "path": path, "document": doc }))
.collect();
serde_json::to_string_pretty(&pairs).map_err(|e| {
BindingCoreError::serialization_failed(format!(
"Failed to serialize well-known documents: {}",
e
))
})
}
#[deprecated(since = "0.9.0", note = "Use sign_artifact() instead")]
pub fn wrap_a2a_artifact(
&self,
artifact_json: &str,
artifact_type: &str,
parent_signatures_json: Option<&str>,
) -> BindingResult<String> {
if std::env::var("JACS_SHOW_DEPRECATIONS").is_ok() {
tracing::warn!("wrap_a2a_artifact is deprecated, use sign_artifact instead");
}
let artifact: Value = serde_json::from_str(artifact_json).map_err(|e| {
BindingCoreError::invalid_argument(format!("Invalid artifact JSON: {}", e))
})?;
let parent_signatures: Option<Vec<Value>> = match parent_signatures_json {
Some(json_str) => {
let parsed: Vec<Value> = serde_json::from_str(json_str).map_err(|e| {
BindingCoreError::invalid_argument(format!(
"Invalid parent signatures JSON array: {}",
e
))
})?;
Some(parsed)
}
None => None,
};
let mut agent = self.lock()?;
let wrapped = jacs::a2a::provenance::wrap_artifact_with_provenance(
&mut agent,
artifact,
artifact_type,
parent_signatures,
)
.map_err(|e| BindingCoreError::signing_failed(format!("Failed to wrap artifact: {}", e)))?;
serde_json::to_string_pretty(&wrapped).map_err(|e| {
BindingCoreError::serialization_failed(format!(
"Failed to serialize wrapped artifact: {}",
e
))
})
}
pub fn sign_artifact(
&self,
artifact_json: &str,
artifact_type: &str,
parent_signatures_json: Option<&str>,
) -> BindingResult<String> {
#[allow(deprecated)]
self.wrap_a2a_artifact(artifact_json, artifact_type, parent_signatures_json)
}
pub fn verify_a2a_artifact(&self, wrapped_json: &str) -> BindingResult<String> {
let wrapped: Value = serde_json::from_str(wrapped_json).map_err(|e| {
BindingCoreError::invalid_argument(format!("Invalid wrapped artifact JSON: {}", e))
})?;
let agent = self.lock()?;
let result =
jacs::a2a::provenance::verify_wrapped_artifact(&agent, &wrapped).map_err(|e| {
BindingCoreError::verification_failed(format!(
"A2A artifact verification error: {}",
e
))
})?;
serde_json::to_string_pretty(&result).map_err(|e| {
BindingCoreError::serialization_failed(format!(
"Failed to serialize verification result: {}",
e
))
})
}
pub fn assess_a2a_agent(&self, agent_card_json: &str, policy: &str) -> BindingResult<String> {
use jacs::a2a::AgentCard;
use jacs::a2a::trust::{A2ATrustPolicy, assess_a2a_agent};
let card: AgentCard = serde_json::from_str(agent_card_json).map_err(|e| {
BindingCoreError::invalid_argument(format!("Invalid Agent Card JSON: {}", e))
})?;
let trust_policy = A2ATrustPolicy::from_str_loose(policy).map_err(|e| {
BindingCoreError::invalid_argument(format!("Invalid trust policy '{}': {}", policy, e))
})?;
let agent = self.lock()?;
let assessment = assess_a2a_agent(&agent, &card, trust_policy);
serde_json::to_string_pretty(&assessment).map_err(|e| {
BindingCoreError::serialization_failed(format!(
"Failed to serialize trust assessment: {}",
e
))
})
}
pub fn verify_a2a_artifact_with_policy(
&self,
wrapped_json: &str,
agent_card_json: &str,
policy: &str,
) -> BindingResult<String> {
use jacs::a2a::AgentCard;
use jacs::a2a::trust::A2ATrustPolicy;
let wrapped: Value = serde_json::from_str(wrapped_json).map_err(|e| {
BindingCoreError::invalid_argument(format!("Invalid wrapped artifact JSON: {}", e))
})?;
let card: AgentCard = serde_json::from_str(agent_card_json).map_err(|e| {
BindingCoreError::invalid_argument(format!("Invalid Agent Card JSON: {}", e))
})?;
let trust_policy = A2ATrustPolicy::from_str_loose(policy).map_err(|e| {
BindingCoreError::invalid_argument(format!("Invalid trust policy '{}': {}", policy, e))
})?;
let agent = self.lock()?;
let result = jacs::a2a::provenance::verify_wrapped_artifact_with_policy(
&agent,
&wrapped,
&card,
trust_policy,
)
.map_err(|e| {
BindingCoreError::verification_failed(format!(
"A2A artifact verification with policy error: {}",
e
))
})?;
serde_json::to_string_pretty(&result).map_err(|e| {
BindingCoreError::serialization_failed(format!(
"Failed to serialize verification result: {}",
e
))
})
}
}
impl AgentWrapper {
#[cfg(feature = "attestation")]
pub fn create_attestation(&self, params_json: &str) -> BindingResult<String> {
use jacs::attestation::AttestationTraits;
use jacs::attestation::types::*;
let params: Value = serde_json::from_str(params_json).map_err(|e| {
BindingCoreError::serialization_failed(format!(
"Failed to parse attestation params JSON: {}. \
Provide a valid JSON object with 'subject' and 'claims' fields.",
e
))
})?;
let subject: AttestationSubject =
serde_json::from_value(params.get("subject").cloned().ok_or_else(|| {
BindingCoreError::validation(
"Missing required 'subject' field in attestation params",
)
})?)
.map_err(|e| BindingCoreError::validation(format!("Invalid 'subject' field: {}", e)))?;
let claims: Vec<Claim> =
serde_json::from_value(params.get("claims").cloned().ok_or_else(|| {
BindingCoreError::validation(
"Missing required 'claims' field in attestation params",
)
})?)
.map_err(|e| BindingCoreError::validation(format!("Invalid 'claims' field: {}", e)))?;
let evidence: Vec<EvidenceRef> = if let Some(ev) = params.get("evidence") {
serde_json::from_value(ev.clone()).map_err(|e| {
BindingCoreError::validation(format!("Invalid 'evidence' field: {}", e))
})?
} else {
vec![]
};
let derivation: Option<Derivation> = if let Some(d) = params.get("derivation") {
Some(serde_json::from_value(d.clone()).map_err(|e| {
BindingCoreError::validation(format!("Invalid 'derivation' field: {}", e))
})?)
} else {
None
};
let policy_context: Option<PolicyContext> = if let Some(p) = params.get("policyContext") {
Some(serde_json::from_value(p.clone()).map_err(|e| {
BindingCoreError::validation(format!("Invalid 'policyContext' field: {}", e))
})?)
} else {
None
};
let mut agent = self.lock()?;
let jacs_doc = agent
.create_attestation(
&subject,
&claims,
&evidence,
derivation.as_ref(),
policy_context.as_ref(),
)
.map_err(|e| {
BindingCoreError::document_failed(format!("Failed to create attestation: {}", e))
})?;
serde_json::to_string_pretty(&jacs_doc.value).map_err(|e| {
BindingCoreError::serialization_failed(format!(
"Failed to serialize attestation: {}",
e
))
})
}
#[cfg(feature = "attestation")]
pub fn verify_attestation(&self, document_key: &str) -> BindingResult<String> {
let agent = self.lock()?;
let result = agent
.verify_attestation_local_impl(document_key)
.map_err(|e| {
BindingCoreError::verification_failed(format!(
"Attestation local verification failed: {}",
e
))
})?;
serde_json::to_string_pretty(&result).map_err(|e| {
BindingCoreError::serialization_failed(format!(
"Failed to serialize verification result: {}",
e
))
})
}
#[cfg(feature = "attestation")]
pub fn verify_attestation_full(&self, document_key: &str) -> BindingResult<String> {
let agent = self.lock()?;
let result = agent
.verify_attestation_full_impl(document_key)
.map_err(|e| {
BindingCoreError::verification_failed(format!(
"Attestation full verification failed: {}",
e
))
})?;
serde_json::to_string_pretty(&result).map_err(|e| {
BindingCoreError::serialization_failed(format!(
"Failed to serialize verification result: {}",
e
))
})
}
#[cfg(feature = "attestation")]
pub fn lift_to_attestation(
&self,
signed_doc_json: &str,
claims_json: &str,
) -> BindingResult<String> {
use jacs::attestation::types::Claim;
let claims: Vec<Claim> = serde_json::from_str(claims_json).map_err(|e| {
BindingCoreError::serialization_failed(format!(
"Failed to parse claims JSON: {}. \
Provide a valid JSON array of claim objects.",
e
))
})?;
let mut agent = self.lock()?;
let jacs_doc =
jacs::attestation::migration::lift_to_attestation(&mut agent, signed_doc_json, &claims)
.map_err(|e| {
BindingCoreError::document_failed(format!(
"Failed to lift document to attestation: {}",
e
))
})?;
serde_json::to_string_pretty(&jacs_doc.value).map_err(|e| {
BindingCoreError::serialization_failed(format!(
"Failed to serialize attestation: {}",
e
))
})
}
#[cfg(feature = "attestation")]
pub fn export_attestation_dsse(&self, attestation_json: &str) -> BindingResult<String> {
let att_value: Value = serde_json::from_str(attestation_json).map_err(|e| {
BindingCoreError::serialization_failed(format!(
"Failed to parse attestation JSON: {}",
e
))
})?;
let envelope = jacs::attestation::dsse::export_dsse(&att_value).map_err(|e| {
BindingCoreError::document_failed(format!("Failed to export DSSE envelope: {}", e))
})?;
serde_json::to_string_pretty(&envelope).map_err(|e| {
BindingCoreError::serialization_failed(format!(
"Failed to serialize DSSE envelope: {}",
e
))
})
}
pub fn build_auth_header(&self) -> BindingResult<String> {
let mut agent = self.lock()?;
jacs::protocol::build_auth_header(&mut agent).map_err(|e| {
BindingCoreError::signing_failed(format!("Failed to build auth header: {}", e))
})
}
pub fn canonicalize_json(&self, json_string: &str) -> BindingResult<String> {
let value: Value = serde_json::from_str(json_string).map_err(|e| {
BindingCoreError::serialization_failed(format!(
"Failed to parse JSON for canonicalization: {}",
e
))
})?;
Ok(jacs::protocol::canonicalize_json(&value))
}
pub fn sign_response(&self, payload_json: &str) -> BindingResult<String> {
let mut agent = self.lock()?;
let payload: Value = serde_json::from_str(payload_json).map_err(|e| {
BindingCoreError::serialization_failed(format!(
"Failed to parse payload JSON for sign_response: {}",
e
))
})?;
let result = jacs::protocol::sign_response(&mut agent, &payload).map_err(|e| {
BindingCoreError::signing_failed(format!("Failed to sign response: {}", e))
})?;
serde_json::to_string(&result).map_err(|e| {
BindingCoreError::serialization_failed(format!(
"Failed to serialize signed response: {}",
e
))
})
}
pub fn encode_verify_payload(&self, document: &str) -> BindingResult<String> {
Ok(jacs::protocol::encode_verify_payload(document))
}
pub fn decode_verify_payload(&self, encoded: &str) -> BindingResult<String> {
jacs::protocol::decode_verify_payload(encoded).map_err(|e| {
BindingCoreError::serialization_failed(format!(
"Failed to decode verify payload: {}",
e
))
})
}
pub fn extract_document_id(&self, document: &str) -> BindingResult<String> {
jacs::protocol::extract_document_id(document)
.map_err(|e| BindingCoreError::generic(format!("Failed to extract document ID: {}", e)))
}
pub fn unwrap_signed_event(
&self,
event_json: &str,
server_keys_json: &str,
) -> BindingResult<String> {
let agent = self.lock()?;
let event: Value = serde_json::from_str(event_json).map_err(|e| {
BindingCoreError::serialization_failed(format!(
"Failed to parse event JSON for unwrap_signed_event: {}",
e
))
})?;
let keys_map: HashMap<String, String> =
serde_json::from_str(server_keys_json).map_err(|e| {
BindingCoreError::serialization_failed(format!(
"Failed to parse server keys JSON for unwrap_signed_event: {}",
e
))
})?;
let keys: HashMap<String, Vec<u8>> = keys_map
.into_iter()
.map(|(k, v)| {
let bytes = base64::engine::general_purpose::STANDARD
.decode(&v)
.unwrap_or_else(|_| v.into_bytes());
(k, bytes)
})
.collect();
let (data, verified) =
jacs::protocol::unwrap_signed_event(&agent, &event, &keys).map_err(|e| {
BindingCoreError::verification_failed(format!(
"Failed to unwrap signed event: {}",
e
))
})?;
let result = json!({"data": data, "verified": verified});
serde_json::to_string(&result).map_err(|e| {
BindingCoreError::serialization_failed(format!(
"Failed to serialize unwrap_signed_event result: {}",
e
))
})
}
}
pub fn diagnostics_standalone() -> String {
serde_json::to_string_pretty(&jacs::simple::diagnostics()).unwrap_or_default()
}
#[derive(Debug, Clone)]
pub struct VerificationResult {
pub valid: bool,
pub signer_id: String,
pub timestamp: String,
pub agent_version: String,
}
pub fn verify_document_standalone(
signed_document: &str,
key_resolution: Option<&str>,
data_directory: Option<&str>,
key_directory: Option<&str>,
) -> BindingResult<VerificationResult> {
use std::collections::HashSet;
use std::path::{Path, PathBuf};
use std::sync::{Mutex, OnceLock};
fn absolutize_dir(raw: &str) -> String {
let p = PathBuf::from(raw);
if p.is_absolute() {
p.to_string_lossy().to_string()
} else {
std::env::current_dir()
.unwrap_or_else(|_| PathBuf::from("."))
.join(p)
.to_string_lossy()
.to_string()
}
}
fn sig_field(doc: &str, field: &str) -> String {
serde_json::from_str::<Value>(doc)
.ok()
.and_then(|v| {
v.get("jacsSignature")
.and_then(|s| s.get(field))
.and_then(|f| f.as_str())
.map(String::from)
})
.unwrap_or_default()
}
fn has_local_key_cache(root: &Path, key_hash: &str) -> bool {
if key_hash.is_empty() {
return false;
}
root.join("public_keys")
.join(format!("{}.pem", key_hash))
.exists()
&& root
.join("public_keys")
.join(format!("{}.enc_type", key_hash))
.exists()
}
fn build_fixture_key_cache(cache_root: &Path, source_dirs: &[PathBuf]) -> usize {
let public_keys_dir = cache_root.join("public_keys");
if std::fs::create_dir_all(&public_keys_dir).is_err() {
return 0;
}
let mut written: HashSet<String> = HashSet::new();
for dir in source_dirs {
let entries = match std::fs::read_dir(dir) {
Ok(v) => v,
Err(_) => continue,
};
for entry in entries.flatten() {
let path = entry.path();
if !path.is_file() {
continue;
}
let Some(name) = path.file_name().and_then(|s| s.to_str()) else {
continue;
};
let Some(prefix) = name.strip_suffix("_metadata.json") else {
continue;
};
let metadata = match std::fs::read_to_string(&path)
.ok()
.and_then(|s| serde_json::from_str::<Value>(&s).ok())
{
Some(v) => v,
None => continue,
};
let key_hash = metadata
.get("public_key_hash")
.and_then(|v| v.as_str())
.unwrap_or("")
.trim();
let signing_algorithm = metadata
.get("signing_algorithm")
.and_then(|v| v.as_str())
.unwrap_or("")
.trim();
if key_hash.is_empty() || signing_algorithm.is_empty() {
continue;
}
if written.contains(key_hash) {
continue;
}
let key_path = dir.join(format!("{}_public_key.pem", prefix));
let key_bytes = match std::fs::read(&key_path) {
Ok(v) => v,
Err(_) => continue,
};
if std::fs::write(public_keys_dir.join(format!("{}.pem", key_hash)), key_bytes)
.is_err()
{
continue;
}
if std::fs::write(
public_keys_dir.join(format!("{}.enc_type", key_hash)),
signing_algorithm.as_bytes(),
)
.is_err()
{
continue;
}
written.insert(key_hash.to_string());
}
}
written.len()
}
fn standalone_verify_lock() -> &'static Mutex<()> {
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
LOCK.get_or_init(|| Mutex::new(()))
}
let _lock = standalone_verify_lock()
.lock()
.map_err(|e| BindingCoreError::generic(format!("Failed to lock standalone verify: {e}")))?;
let signer_id = sig_field(signed_document, "agentID");
let timestamp = sig_field(signed_document, "date");
let agent_version = sig_field(signed_document, "agentVersion");
let signer_public_key_hash = sig_field(signed_document, "publicKeyHash");
let temp_dir = std::env::temp_dir().to_string_lossy().to_string();
let raw_data_dir = data_directory
.map(String::from)
.unwrap_or_else(|| temp_dir.clone());
let raw_key_dir = key_directory
.map(String::from)
.unwrap_or_else(|| raw_data_dir.clone());
let absolute_data_dir = absolutize_dir(&raw_data_dir);
let absolute_key_dir = absolutize_dir(&raw_key_dir);
let mut effective_storage_root = if data_directory.is_some() {
absolute_data_dir.clone()
} else if key_directory.is_some() {
absolute_key_dir.clone()
} else {
absolute_data_dir.clone()
};
let mut temp_cache_root: Option<PathBuf> = None;
let local_requested = key_resolution.map_or(true, |kr| {
kr.split(',')
.any(|part| part.trim().eq_ignore_ascii_case("local"))
});
if local_requested && !signer_public_key_hash.is_empty() {
let current_root = PathBuf::from(&effective_storage_root);
if !has_local_key_cache(¤t_root, &signer_public_key_hash) {
let mut source_dirs = Vec::new();
let data_path = PathBuf::from(&absolute_data_dir);
let key_path = PathBuf::from(&absolute_key_dir);
if data_path.exists() {
source_dirs.push(data_path);
}
if key_path.exists() && !source_dirs.iter().any(|p| p == &key_path) {
source_dirs.push(key_path);
}
if !source_dirs.is_empty() {
let nonce = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let cache_root = std::env::temp_dir().join(format!(
"jacs_standalone_keycache_{}_{}",
std::process::id(),
nonce
));
let _ = build_fixture_key_cache(&cache_root, &source_dirs);
if has_local_key_cache(&cache_root, &signer_public_key_hash) {
effective_storage_root = cache_root.to_string_lossy().to_string();
temp_cache_root = Some(cache_root);
} else {
let _ = std::fs::remove_dir_all(&cache_root);
}
}
}
}
let explicit_local_key_available = local_requested
&& !signer_public_key_hash.is_empty()
&& has_local_key_cache(
&PathBuf::from(&effective_storage_root),
&signer_public_key_hash,
);
let data_dir = String::new();
let key_dir = String::new();
let config = Config::new(
Some("false".to_string()),
Some(data_dir.clone()),
Some(key_dir.clone()),
Some("jacs.private.pem.enc".to_string()),
Some("jacs.public.pem".to_string()),
Some("pq2025".to_string()),
None,
Some("".to_string()),
Some("fs".to_string()),
);
let config_json = serde_json::to_string_pretty(&config).map_err(|e| {
BindingCoreError::serialization_failed(format!("Failed to serialize config: {}", e))
})?;
let thread_id = format!("{:?}", std::thread::current().id())
.chars()
.map(|c| if c.is_ascii_alphanumeric() { c } else { '_' })
.collect::<String>();
let nonce = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let config_path = std::env::temp_dir().join(format!(
"jacs_standalone_verify_config_{}_{}_{}.json",
std::process::id(),
thread_id,
nonce
));
std::fs::write(&config_path, &config_json)
.map_err(|e| BindingCoreError::generic(format!("Failed to write temp config: {}", e)))?;
use jacs::storage::jenv;
struct KeyResolutionGuard {
had_override: bool,
prev_value: Option<String>,
}
impl Drop for KeyResolutionGuard {
fn drop(&mut self) {
if self.had_override {
if let Some(ref val) = self.prev_value {
let _ = jacs::storage::jenv::set_env_var("JACS_KEY_RESOLUTION", val);
} else {
let _ = jacs::storage::jenv::clear_env_var("JACS_KEY_RESOLUTION");
}
} else {
let _ = jacs::storage::jenv::clear_env_var("JACS_KEY_RESOLUTION");
}
}
}
let kr_had_override = jenv::has_jenv_override("JACS_KEY_RESOLUTION");
let kr_prev = if kr_had_override {
jenv::get_env_var("JACS_KEY_RESOLUTION", false)
.ok()
.flatten()
} else {
None
};
if let Some(kr) = key_resolution {
let _ = jenv::set_env_var("JACS_KEY_RESOLUTION", kr);
} else {
let _ = jenv::clear_env_var("JACS_KEY_RESOLUTION");
}
let _kr_guard = KeyResolutionGuard {
had_override: kr_had_override,
prev_value: kr_prev,
};
let result: BindingResult<VerificationResult> = (|| {
let wrapper = AgentWrapper::new();
wrapper.load_file_only(config_path.to_string_lossy().to_string())?;
let _ = wrapper.set_storage_root(PathBuf::from(&effective_storage_root));
if explicit_local_key_available {
let key_base = PathBuf::from(&effective_storage_root)
.join("public_keys")
.join(&signer_public_key_hash);
let public_key = std::fs::read(key_base.with_extension("pem")).map_err(|e| {
BindingCoreError::verification_failed(format!(
"Failed to load local public key for hash '{}': {}",
signer_public_key_hash, e
))
})?;
let enc_type = std::fs::read_to_string(key_base.with_extension("enc_type"))
.map_err(|e| {
BindingCoreError::verification_failed(format!(
"Failed to load local public key type for hash '{}': {}",
signer_public_key_hash, e
))
})?
.trim()
.to_string();
let mut agent = wrapper.lock()?;
let doc = agent.load_document(signed_document).map_err(|e| {
BindingCoreError::document_failed(format!("Failed to load document: {}", e))
})?;
let document_key = doc.getkey();
let value = doc.getvalue();
agent.verify_hash(value).map_err(|e| {
BindingCoreError::verification_failed(format!(
"Failed to verify document hash: {}",
e
))
})?;
agent
.verify_document_signature(
&document_key,
None,
None,
Some(public_key),
Some(enc_type.clone()),
)
.map_err(|e| {
BindingCoreError::verification_failed(format!(
"Failed to verify document signature (enc_type={}): {}",
enc_type, e
))
})?;
return Ok(VerificationResult {
valid: true,
signer_id: signer_id.clone(),
timestamp: timestamp.clone(),
agent_version: agent_version.clone(),
});
}
let valid = wrapper.verify_document(signed_document)?;
Ok(VerificationResult {
valid,
signer_id: signer_id.clone(),
timestamp: timestamp.clone(),
agent_version: agent_version.clone(),
})
})();
let _ = std::fs::remove_file(&config_path);
if let Some(cache_root) = temp_cache_root {
let _ = std::fs::remove_dir_all(cache_root);
}
match result {
Ok(r) => Ok(r),
Err(e) => {
if e.kind == ErrorKind::VerificationFailed
|| e.kind == ErrorKind::DocumentFailed
|| e.kind == ErrorKind::InvalidArgument
{
Ok(VerificationResult {
valid: false,
signer_id,
timestamp,
agent_version,
})
} else {
Err(e)
}
}
}
}
pub fn hash_string(data: &str) -> String {
jacs_hash_string(&data.to_string())
}
pub fn hash_public_key_base64(public_key_b64: &str) -> BindingResult<String> {
let public_key = decode_public_key_base64(public_key_b64)?;
Ok(jacs::crypt::hash::hash_public_key(&public_key))
}
pub fn build_jwk_set_from_public_key(
public_key_b64: &str,
key_algorithm: &str,
key_id: &str,
) -> BindingResult<String> {
let public_key = decode_public_key_base64(public_key_b64)?;
let jwk_set = build_jwk_set_from_public_key_bytes(&public_key, key_algorithm, key_id)?;
serde_json::to_string(&jwk_set).map_err(|e| {
BindingCoreError::serialization_failed(format!("Failed to serialize JWK set: {}", e))
})
}
pub fn ensure_network_access(capability: &str) -> BindingResult<()> {
let capability = jacs::config::NetworkCapability::from_str(capability)
.map_err(BindingCoreError::invalid_argument)?;
jacs::config::ensure_network_access(capability)
.map_err(|e| BindingCoreError::network_failed(e.to_string()))
}
pub fn fetch_agent_card(base_url: &str, timeout_ms: Option<u64>) -> BindingResult<String> {
let trimmed = base_url.trim();
if trimmed.is_empty() {
return Err(BindingCoreError::invalid_argument(
"Agent base URL cannot be empty",
));
}
let card_url = format!(
"{}/.well-known/agent-card.json",
trimmed.trim_end_matches('/')
);
let parsed_url = Url::parse(&card_url).map_err(|e| {
BindingCoreError::invalid_argument(format!("Invalid agent URL '{}': {}", base_url, e))
})?;
validate_network_url(&parsed_url, "Agent Card URL")?;
jacs::config::ensure_network_access(jacs::config::NetworkCapability::AgentCardFetch)
.map_err(|e| BindingCoreError::network_failed(e.to_string()))?;
let client = build_blocking_json_client(timeout_ms.unwrap_or(DEFAULT_NETWORK_TIMEOUT_MS))?;
let response = client
.get(parsed_url.clone())
.header(ACCEPT, "application/json")
.send()
.map_err(|e| {
if e.is_timeout() {
BindingCoreError::network_failed(format!(
"Agent discovery timed out: {}",
parsed_url
))
} else {
BindingCoreError::network_failed(format!(
"Agent unreachable: {} ({})",
parsed_url, e
))
}
})?;
if response.status() == StatusCode::NOT_FOUND {
return Err(BindingCoreError::network_failed(format!(
"Agent card not found (404): {}",
parsed_url
)));
}
if !response.status().is_success() {
return Err(BindingCoreError::network_failed(format!(
"Agent card request failed (HTTP {}): {}",
response.status(),
parsed_url
)));
}
let content_type = content_type_header(&response);
if !content_type.is_empty() && !content_type.to_ascii_lowercase().contains("json") {
return Err(BindingCoreError::validation(format!(
"Agent card response is not JSON (content-type: {}): {}",
content_type, parsed_url
)));
}
let body = response.text().map_err(|e| {
BindingCoreError::network_failed(format!(
"Failed to read Agent Card response from {}: {}",
parsed_url, e
))
})?;
parse_json_object_body(
&body,
format!("Agent card is not valid JSON: {}", parsed_url),
format!("Agent card at {} is not a JSON object", parsed_url),
)
}
pub fn fetch_remote_key_lookup(
base_url: Option<&str>,
jacs_id: Option<&str>,
version: Option<&str>,
public_key_hash: Option<&str>,
timeout_ms: Option<u64>,
) -> BindingResult<String> {
let resolved_base_url = resolve_keys_base_url(base_url);
let mut parsed_url = Url::parse(&resolved_base_url).map_err(|e| {
BindingCoreError::invalid_argument(format!(
"Invalid JACS key base URL '{}': {}",
resolved_base_url, e
))
})?;
validate_network_url(&parsed_url, "JACS key lookup base URL")?;
{
let mut segments = parsed_url.path_segments_mut().map_err(|_| {
BindingCoreError::invalid_argument(format!(
"Invalid JACS key base URL '{}': cannot append path segments",
resolved_base_url
))
})?;
if let Some(hash) = public_key_hash
.map(str::trim)
.filter(|value| !value.is_empty())
{
let normalized_hash = normalize_public_key_hash(hash)?;
segments.extend(["jacs", "v1", "keys", "by-hash"]);
segments.push(&normalized_hash);
} else {
let agent_id = jacs_id
.map(str::trim)
.filter(|value| !value.is_empty())
.ok_or_else(|| {
BindingCoreError::invalid_argument(
"fetch_remote_key_lookup requires jacs_id or public_key_hash",
)
})?;
let resolved_version = version
.map(str::trim)
.filter(|value| !value.is_empty())
.unwrap_or("latest");
segments.extend(["jacs", "v1", "agents"]);
segments.push(agent_id);
segments.push("keys");
segments.push(resolved_version);
}
}
jacs::config::ensure_network_access(jacs::config::NetworkCapability::RemoteKeyFetch)
.map_err(|e| BindingCoreError::network_failed(e.to_string()))?;
let client = build_blocking_json_client(timeout_ms.unwrap_or(DEFAULT_NETWORK_TIMEOUT_MS))?;
let response = client
.get(parsed_url.clone())
.header(ACCEPT, "application/json")
.send()
.map_err(|e| {
if e.is_timeout() {
BindingCoreError::network_failed(format!(
"Remote key lookup timed out: {}",
parsed_url
))
} else {
BindingCoreError::network_failed(format!(
"Failed to reach key lookup endpoint {}: {}",
parsed_url, e
))
}
})?;
let status = response.status();
let content_type = content_type_header(&response);
let body = response.text().map_err(|e| {
BindingCoreError::network_failed(format!(
"Failed to read key lookup response from {}: {}",
parsed_url, e
))
})?;
if !status.is_success() {
let detail = if body.trim().is_empty() {
status.canonical_reason().unwrap_or("unknown error")
} else {
body.trim()
};
return Err(BindingCoreError::network_failed(format!(
"HTTP {} from key lookup endpoint: {}",
status.as_u16(),
detail
)));
}
if !content_type.is_empty() && !content_type.to_ascii_lowercase().contains("json") {
return Err(BindingCoreError::validation(format!(
"Key lookup endpoint returned non-JSON response: {}",
parsed_url
)));
}
parse_json_object_body(
&body,
format!(
"Key lookup endpoint returned non-JSON response: {}",
parsed_url
),
format!(
"Key lookup endpoint returned a non-object response: {}",
parsed_url
),
)
}
pub fn resolve_private_key_password(
config_path: Option<&str>,
key_directory: Option<&str>,
explicit_password: Option<&str>,
) -> BindingResult<String> {
if let Some(password) = explicit_password {
if password.trim().is_empty() {
return Err(BindingCoreError::invalid_argument(
"Explicit password provided but empty or whitespace-only.",
));
}
return Ok(password.to_string());
}
if let Ok(password) = std::env::var("JACS_PRIVATE_KEY_PASSWORD") {
if password.trim().is_empty() {
return Err(BindingCoreError::invalid_argument(
"JACS_PRIVATE_KEY_PASSWORD is set but empty or whitespace-only.",
));
}
return Ok(password);
}
let (resolved_key_directory, agent_id) = resolve_password_context(config_path, key_directory)?;
match jacs::crypt::aes_encrypt::resolve_private_key_password(None, agent_id.as_deref()) {
Ok(password) => Ok(password),
Err(e) if missing_password_message(&e.to_string()) => Ok(read_password_file(
&resolved_key_directory.join(".jacs_password"),
)?
.unwrap_or_default()),
Err(e) => Err(BindingCoreError::generic(format!(
"Failed to resolve private key password: {}",
e
))),
}
}
pub fn quickstart_private_key_password(
config_path: Option<&str>,
key_directory: Option<&str>,
) -> BindingResult<String> {
let existing = resolve_private_key_password(config_path, key_directory, None)?;
if !existing.is_empty() {
return Ok(existing);
}
let password = generate_private_key_password_value();
if truthy_env_var("JACS_SAVE_PASSWORD_FILE") {
let (resolved_key_directory, _agent_id) =
resolve_password_context(config_path, key_directory)?;
persist_password_file(&resolved_key_directory, &password)?;
}
Ok(password)
}
pub fn create_config(
jacs_use_security: Option<String>,
jacs_data_directory: Option<String>,
jacs_key_directory: Option<String>,
jacs_agent_private_key_filename: Option<String>,
jacs_agent_public_key_filename: Option<String>,
jacs_agent_key_algorithm: Option<String>,
jacs_private_key_password: Option<String>,
jacs_agent_id_and_version: Option<String>,
jacs_default_storage: Option<String>,
) -> BindingResult<String> {
let config = Config::new(
jacs_use_security,
jacs_data_directory,
jacs_key_directory,
jacs_agent_private_key_filename,
jacs_agent_public_key_filename,
jacs_agent_key_algorithm,
jacs_private_key_password,
jacs_agent_id_and_version,
jacs_default_storage,
);
serde_json::to_string_pretty(&config).map_err(|e| {
BindingCoreError::serialization_failed(format!("Failed to serialize config: {}", e))
})
}
pub fn trust_agent(agent_json: &str) -> BindingResult<String> {
jacs::trust::trust_agent(agent_json)
.map_err(|e| BindingCoreError::trust_failed(format!("Failed to trust agent: {}", e)))
}
pub fn trust_agent_with_key(agent_json: &str, public_key_pem: &str) -> BindingResult<String> {
if public_key_pem.trim().is_empty() {
return Err(BindingCoreError::invalid_argument(
"public_key_pem cannot be empty",
));
}
jacs::trust::trust_agent_with_key(agent_json, Some(public_key_pem)).map_err(|e| {
BindingCoreError::trust_failed(format!("Failed to trust agent with explicit key: {}", e))
})
}
pub fn list_trusted_agents() -> BindingResult<Vec<String>> {
jacs::trust::list_trusted_agents().map_err(|e| {
BindingCoreError::trust_failed(format!("Failed to list trusted agents: {}", e))
})
}
pub fn untrust_agent(agent_id: &str) -> BindingResult<()> {
jacs::trust::untrust_agent(agent_id)
.map_err(|e| BindingCoreError::trust_failed(format!("Failed to untrust agent: {}", e)))
}
pub fn is_trusted(agent_id: &str) -> bool {
jacs::trust::is_trusted(agent_id)
}
pub fn get_trusted_agent(agent_id: &str) -> BindingResult<String> {
jacs::trust::get_trusted_agent(agent_id)
.map_err(|e| BindingCoreError::trust_failed(format!("Failed to get trusted agent: {}", e)))
}
pub fn audit(config_path: Option<&str>, recent_n: Option<u32>) -> BindingResult<String> {
use jacs::audit::{AuditOptions, audit as jacs_audit};
let mut opts = AuditOptions::default();
opts.config_path = config_path.map(String::from);
if let Some(n) = recent_n {
opts.recent_verify_count = Some(n);
}
let result =
jacs_audit(opts).map_err(|e| BindingCoreError::generic(format!("Audit failed: {}", e)))?;
serde_json::to_string_pretty(&result).map_err(|e| {
BindingCoreError::serialization_failed(format!("Failed to serialize audit result: {}", e))
})
}
pub fn create_agent_programmatic(
name: &str,
password: &str,
algorithm: Option<&str>,
data_directory: Option<&str>,
key_directory: Option<&str>,
config_path: Option<&str>,
agent_type: Option<&str>,
description: Option<&str>,
domain: Option<&str>,
default_storage: Option<&str>,
) -> BindingResult<String> {
use jacs::simple::{CreateAgentParams, SimpleAgent};
let params = CreateAgentParams {
name: name.to_string(),
password: password.to_string(),
algorithm: algorithm.unwrap_or("pq2025").to_string(),
data_directory: data_directory.unwrap_or("./jacs_data").to_string(),
key_directory: key_directory.unwrap_or("./jacs_keys").to_string(),
config_path: config_path.unwrap_or("./jacs.config.json").to_string(),
agent_type: agent_type.unwrap_or("ai").to_string(),
description: description.unwrap_or("").to_string(),
domain: domain.unwrap_or("").to_string(),
default_storage: default_storage.unwrap_or("fs").to_string(),
storage: None,
};
let (_agent, info) = SimpleAgent::create_with_params(params)
.map_err(|e| BindingCoreError::agent_load(format!("Failed to create agent: {}", e)))?;
serde_json::to_string_pretty(&info).map_err(|e| {
BindingCoreError::serialization_failed(format!("Failed to serialize agent info: {}", e))
})
}
pub fn handle_agent_create(filename: Option<&String>, create_keys: bool) -> BindingResult<()> {
jacs::cli_utils::create::handle_agent_create(filename, create_keys)
.map_err(|e| BindingCoreError::generic(e.to_string()))
}
pub fn handle_agent_create_auto(
filename: Option<&String>,
create_keys: bool,
auto_update_config: bool,
) -> BindingResult<()> {
jacs::cli_utils::create::handle_agent_create_auto(filename, create_keys, auto_update_config)
.map_err(|e| BindingCoreError::generic(e.to_string()))
}
pub fn handle_config_create() -> BindingResult<()> {
jacs::cli_utils::create::handle_config_create()
.map_err(|e| BindingCoreError::generic(e.to_string()))
}
pub use jacs::dns::bootstrap::DnsVerificationResult;
pub fn verify_agent_dns(agent_json: &str, domain: &str) -> BindingResult<DnsVerificationResult> {
jacs::dns::bootstrap::verify_agent_dns(agent_json, domain).map_err(|e| {
BindingCoreError::invalid_argument(format!("DNS verification setup failed: {}", e))
})
}
pub use jacs;
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
use tempfile::tempdir;
fn cross_language_fixtures_dir() -> Option<PathBuf> {
let workspace = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()?
.to_path_buf();
let dir = workspace.join("jacs/tests/fixtures/cross-language");
if dir.exists() { Some(dir) } else { None }
}
#[test]
fn verify_standalone_invalid_json_returns_valid_false() {
let result = verify_document_standalone("not json", Some("local"), None, None).unwrap();
assert!(!result.valid);
assert_eq!(result.signer_id, "");
}
#[test]
fn verify_standalone_tampered_document_returns_valid_false_with_signer_id() {
let tampered = r#"{"jacsSignature":{"agentID":"golden-test-agent","agentVersion":"v1"},"jacsSha256":"x"}"#;
let result = verify_document_standalone(tampered, Some("local"), None, None).unwrap();
assert!(!result.valid);
assert_eq!(result.signer_id, "golden-test-agent");
}
#[test]
fn verify_standalone_golden_invalid_signature_returns_valid_false() {
let invalid_sig =
std::fs::read_to_string("../jacs/tests/fixtures/golden/invalid_signature.json")
.unwrap_or_else(|_| {
r#"{"jacsSignature":{"agentID":"golden-test-agent"},"jacsSha256":"x"}"#
.to_string()
});
let result = verify_document_standalone(
&invalid_sig,
Some("local"),
Some("../jacs/tests/fixtures"),
Some("../jacs/tests/fixtures/keys"),
)
.unwrap();
assert!(!result.valid);
assert_eq!(result.signer_id, "golden-test-agent");
}
#[test]
fn verify_standalone_nonexistent_key_directory_returns_valid_false() {
let doc = r#"{"jacsSignature":{"agentID":"some-agent"},"jacsSha256":"x"}"#;
let result = verify_document_standalone(
doc,
Some("local"),
Some("/nonexistent_data"),
Some("/nonexistent_keys"),
)
.unwrap();
assert!(!result.valid);
assert_eq!(result.signer_id, "some-agent");
}
#[cfg(unix)]
#[test]
fn read_password_file_rejects_insecure_permissions() {
let tmp = tempdir().expect("tempdir");
let password_path = tmp.path().join(".jacs_password");
fs::write(&password_path, "TopSecret!123").expect("write password file");
fs::set_permissions(&password_path, std::fs::Permissions::from_mode(0o644))
.expect("set insecure permissions");
let result = read_password_file(&password_path);
assert!(result.is_err(), "insecure password file should be rejected");
assert!(
result.unwrap_err().message.contains("insecure permissions"),
"error should mention permissions"
);
}
#[cfg(unix)]
#[test]
fn persist_password_file_creates_owner_only_password_file() {
let tmp = tempdir().expect("tempdir");
persist_password_file(tmp.path(), "TopSecret!123").expect("persist password file");
let password_path = tmp.path().join(".jacs_password");
let metadata = fs::metadata(&password_path).expect("stat password file");
let mode = metadata.permissions().mode() & 0o777;
assert_eq!(mode, 0o600, "password file should be created with 0600");
let saved = fs::read_to_string(&password_path).expect("read password file");
assert_eq!(saved, "TopSecret!123");
}
#[test]
#[ignore = "pre-existing: cross-language fixture verification fails with relative parent paths"]
fn verify_standalone_accepts_relative_parent_paths_from_subdir() {
let Some(fixtures_dir) = cross_language_fixtures_dir() else {
eprintln!("Skipping: cross-language fixtures directory not found");
return;
};
let signed_path = fixtures_dir.join("python_ed25519_signed.json");
if !signed_path.exists() {
eprintln!(
"Skipping: fixture '{}' not found",
signed_path.to_string_lossy()
);
return;
}
let signed = std::fs::read_to_string(&signed_path).expect("read python fixture");
let workspace = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.expect("workspace root")
.to_path_buf();
let jacsnpm_dir = workspace.join("jacsnpm");
if !jacsnpm_dir.exists() {
eprintln!("Skipping: jacsnpm directory not found");
return;
}
struct CwdGuard(PathBuf);
impl Drop for CwdGuard {
fn drop(&mut self) {
let _ = std::env::set_current_dir(&self.0);
}
}
let original_cwd = std::env::current_dir().expect("current dir");
std::env::set_current_dir(&jacsnpm_dir).expect("chdir to jacsnpm");
let _guard = CwdGuard(original_cwd);
let rel = "../jacs/tests/fixtures/cross-language";
let result = verify_document_standalone(&signed, Some("local"), Some(rel), Some(rel))
.expect("standalone verify should not error");
assert!(result.valid, "relative parent-path fixture should verify");
}
#[test]
fn verify_standalone_accepts_absolute_fixture_paths() {
let Some(fixtures_dir) = cross_language_fixtures_dir() else {
eprintln!("Skipping: cross-language fixtures directory not found");
return;
};
let signed_path = fixtures_dir.join("python_ed25519_signed.json");
if !signed_path.exists() {
eprintln!(
"Skipping: fixture '{}' not found",
signed_path.to_string_lossy()
);
return;
}
let signed = std::fs::read_to_string(&signed_path).expect("read python fixture");
let fixtures_abs = fixtures_dir
.canonicalize()
.unwrap_or_else(|_| fixtures_dir.clone());
let fixtures_abs_str = fixtures_abs.to_string_lossy().to_string();
let result = verify_document_standalone(
&signed,
Some("local"),
Some(&fixtures_abs_str),
Some(&fixtures_abs_str),
)
.expect("standalone verify should not error");
assert!(result.valid, "absolute-path fixture should verify");
}
#[test]
fn verify_standalone_uses_key_directory_when_data_directory_missing() {
let Some(fixtures_dir) = cross_language_fixtures_dir() else {
eprintln!("Skipping: cross-language fixtures directory not found");
return;
};
let signed_path = fixtures_dir.join("python_ed25519_signed.json");
if !signed_path.exists() {
eprintln!(
"Skipping: fixture '{}' not found",
signed_path.to_string_lossy()
);
return;
}
let signed = std::fs::read_to_string(&signed_path).expect("read python fixture");
let fixtures_abs = fixtures_dir
.canonicalize()
.unwrap_or_else(|_| fixtures_dir.clone());
let fixtures_abs_str = fixtures_abs.to_string_lossy().to_string();
let result =
verify_document_standalone(&signed, Some("local"), None, Some(&fixtures_abs_str))
.expect("standalone verify should not error");
assert!(
result.valid,
"key_directory should be usable as standalone storage root when data_directory is omitted"
);
}
#[test]
fn verify_standalone_ignores_polluting_env_overrides() {
let Some(fixtures_dir) = cross_language_fixtures_dir() else {
eprintln!("Skipping: cross-language fixtures directory not found");
return;
};
let signed_path = fixtures_dir.join("python_ed25519_signed.json");
if !signed_path.exists() {
eprintln!(
"Skipping: fixture '{}' not found",
signed_path.to_string_lossy()
);
return;
}
let signed = std::fs::read_to_string(&signed_path).expect("read python fixture");
let fixtures_abs = fixtures_dir
.canonicalize()
.unwrap_or_else(|_| fixtures_dir.clone());
let fixtures_abs_str = fixtures_abs.to_string_lossy().to_string();
struct EnvRestore(Vec<(&'static str, Option<std::ffi::OsString>)>);
impl Drop for EnvRestore {
fn drop(&mut self) {
for (k, v) in &self.0 {
if let Some(val) = v {
unsafe { std::env::set_var(k, val) }
} else {
unsafe { std::env::remove_var(k) }
}
}
}
}
let keys = [
"JACS_DATA_DIRECTORY",
"JACS_KEY_DIRECTORY",
"JACS_DEFAULT_STORAGE",
"JACS_KEY_RESOLUTION",
];
let mut prev = Vec::new();
for k in keys {
prev.push((k, std::env::var_os(k)));
}
let _restore = EnvRestore(prev);
unsafe {
std::env::set_var("JACS_DATA_DIRECTORY", "/tmp/does-not-exist");
std::env::set_var("JACS_KEY_DIRECTORY", "/tmp/does-not-exist");
std::env::set_var("JACS_DEFAULT_STORAGE", "memory");
std::env::set_var("JACS_KEY_RESOLUTION", "remote");
}
let result = verify_document_standalone(
&signed,
Some("local"),
Some(&fixtures_abs_str),
Some(&fixtures_abs_str),
)
.expect("standalone verify should not error");
assert!(
result.valid,
"verification should ignore ambient JACS_* env pollution"
);
}
#[test]
fn audit_default_returns_ok_json_has_risks_and_health_checks() {
let json = audit(None, None).unwrap();
let v: serde_json::Value = serde_json::from_str(&json).unwrap();
assert!(v.get("risks").is_some(), "audit JSON should have risks");
assert!(
v.get("health_checks").is_some(),
"audit JSON should have health_checks"
);
}
fn ephemeral_wrapper() -> AgentWrapper {
let wrapper = AgentWrapper::new();
wrapper.ephemeral(Some("ed25519")).unwrap();
wrapper
}
#[cfg(feature = "a2a")]
#[test]
fn test_export_agent_card_returns_valid_json() {
let wrapper = ephemeral_wrapper();
let card_json = wrapper.export_agent_card().unwrap();
let card: Value = serde_json::from_str(&card_json).unwrap();
assert!(card.get("name").is_some());
assert!(card.get("protocolVersions").is_some());
assert_eq!(card["protocolVersions"][0], "0.4.0");
}
#[cfg(feature = "a2a")]
#[test]
#[allow(deprecated)]
fn test_wrap_and_verify_a2a_artifact() {
let wrapper = ephemeral_wrapper();
let artifact = r#"{"content": "hello A2A"}"#;
let wrapped = wrapper
.wrap_a2a_artifact(artifact, "message", None)
.unwrap();
let wrapped_value: Value = serde_json::from_str(&wrapped).unwrap();
assert!(wrapped_value.get("jacsId").is_some());
assert_eq!(wrapped_value["jacsType"], "a2a-message");
let result_json = wrapper.verify_a2a_artifact(&wrapped).unwrap();
let result: Value = serde_json::from_str(&result_json).unwrap();
assert_eq!(result["valid"], true);
assert_eq!(result["status"], "SelfSigned");
}
#[cfg(feature = "a2a")]
#[test]
fn test_sign_artifact_alias_matches_wrap() {
let wrapper = ephemeral_wrapper();
let artifact = r#"{"data": 42}"#;
let signed = wrapper.sign_artifact(artifact, "artifact", None).unwrap();
let value: Value = serde_json::from_str(&signed).unwrap();
assert_eq!(value["jacsType"], "a2a-artifact");
let result_json = wrapper.verify_a2a_artifact(&signed).unwrap();
let result: Value = serde_json::from_str(&result_json).unwrap();
assert_eq!(result["valid"], true);
}
#[cfg(feature = "a2a")]
#[test]
#[allow(deprecated)]
fn test_wrap_a2a_artifact_with_parent_chain() {
let wrapper = ephemeral_wrapper();
let first = wrapper
.wrap_a2a_artifact(r#"{"step": 1}"#, "task", None)
.unwrap();
let parents = format!("[{}]", first);
let second = wrapper
.wrap_a2a_artifact(r#"{"step": 2}"#, "task", Some(&parents))
.unwrap();
let second_value: Value = serde_json::from_str(&second).unwrap();
let parent_sigs = second_value["jacsParentSignatures"].as_array().unwrap();
assert_eq!(parent_sigs.len(), 1);
}
#[cfg(feature = "a2a")]
#[test]
#[allow(deprecated)]
fn test_wrap_a2a_artifact_invalid_json_error() {
let wrapper = ephemeral_wrapper();
let result = wrapper.wrap_a2a_artifact("not json", "artifact", None);
assert!(result.is_err());
assert_eq!(result.unwrap_err().kind, ErrorKind::InvalidArgument);
}
#[cfg(feature = "a2a")]
#[test]
fn test_verify_a2a_artifact_invalid_json_error() {
let wrapper = ephemeral_wrapper();
let result = wrapper.verify_a2a_artifact("not json");
assert!(result.is_err());
assert_eq!(result.unwrap_err().kind, ErrorKind::InvalidArgument);
}
#[cfg(feature = "a2a")]
#[test]
fn test_export_agent_card_unloaded_agent_error() {
let wrapper = AgentWrapper::new();
let result = wrapper.export_agent_card();
assert!(result.is_err());
}
fn protocol_wrapper() -> AgentWrapper {
let wrapper = AgentWrapper::new();
wrapper.ephemeral(Some("ed25519")).unwrap();
wrapper
}
#[test]
fn protocol_build_auth_header_starts_with_jacs() {
let wrapper = protocol_wrapper();
let header = wrapper
.build_auth_header()
.expect("build_auth_header failed");
assert!(
header.starts_with("JACS "),
"Header must start with 'JACS ', got: {header}"
);
}
#[test]
fn protocol_canonicalize_json_sorts_keys() {
let wrapper = protocol_wrapper();
let result = wrapper
.canonicalize_json(r#"{"b":1,"a":2}"#)
.expect("canonicalize_json failed");
assert_eq!(result, r#"{"a":2,"b":1}"#);
}
#[test]
fn protocol_canonicalize_json_invalid_input() {
let wrapper = protocol_wrapper();
let result = wrapper.canonicalize_json("not json");
assert!(result.is_err());
assert_eq!(result.unwrap_err().kind, ErrorKind::SerializationFailed);
}
#[test]
fn protocol_sign_response_has_required_fields() {
let wrapper = protocol_wrapper();
let result = wrapper
.sign_response(r#"{"answer": 42}"#)
.expect("sign_response failed");
let envelope: Value = serde_json::from_str(&result).expect("should be valid JSON");
assert!(envelope.get("version").is_some(), "missing 'version'");
assert!(
envelope.get("jacsSignature").is_some(),
"missing 'jacsSignature'"
);
assert_eq!(envelope["version"], "1.0.0");
}
#[test]
fn protocol_sign_response_invalid_payload() {
let wrapper = protocol_wrapper();
let result = wrapper.sign_response("not json");
assert!(result.is_err());
assert_eq!(result.unwrap_err().kind, ErrorKind::SerializationFailed);
}
#[test]
fn protocol_encode_verify_payload_round_trips() {
let wrapper = protocol_wrapper();
let original = r#"{"test":true}"#;
let encoded = wrapper
.encode_verify_payload(original)
.expect("encode_verify_payload failed");
assert!(!encoded.contains('+'), "URL-safe base64 must not contain +");
assert!(!encoded.contains('/'), "URL-safe base64 must not contain /");
assert!(
!encoded.contains('='),
"URL-safe base64 must not have padding"
);
let decoded = wrapper
.decode_verify_payload(&encoded)
.expect("decode_verify_payload failed");
assert_eq!(decoded, original);
}
#[test]
fn protocol_extract_document_id_extracts_id() {
let wrapper = protocol_wrapper();
let id = wrapper
.extract_document_id(r#"{"jacsDocumentId":"abc-123"}"#)
.expect("extract_document_id failed");
assert_eq!(id, "abc-123");
}
#[test]
fn protocol_extract_document_id_no_id_errors() {
let wrapper = protocol_wrapper();
let result = wrapper.extract_document_id(r#"{"name":"no-id"}"#);
assert!(result.is_err());
}
#[test]
fn protocol_unwrap_signed_event_unknown_agent_unverified() {
let wrapper = protocol_wrapper();
let event = r#"{"data":{"result":"hello"},"jacsSignature":{"agentID":"unknown:v1","date":"2026-01-01T00:00:00Z","signature":"fakesig"}}"#;
let keys = r#"{}"#;
let result = wrapper
.unwrap_signed_event(event, keys)
.expect("unwrap_signed_event failed");
let parsed: Value = serde_json::from_str(&result).expect("should be valid JSON");
assert_eq!(parsed["verified"], false);
assert_eq!(parsed["data"]["result"], "hello");
}
#[test]
fn protocol_unwrap_signed_event_legacy_payload() {
let wrapper = protocol_wrapper();
let event = r#"{"payload":{"status":"ok"}}"#;
let keys = r#"{}"#;
let result = wrapper
.unwrap_signed_event(event, keys)
.expect("unwrap_signed_event failed");
let parsed: Value = serde_json::from_str(&result).expect("should be valid JSON");
assert_eq!(parsed["verified"], false);
assert_eq!(parsed["data"]["status"], "ok");
}
#[test]
fn protocol_unwrap_signed_event_plain_event() {
let wrapper = protocol_wrapper();
let event = r#"{"type":"heartbeat","ts":12345}"#;
let keys = r#"{}"#;
let result = wrapper
.unwrap_signed_event(event, keys)
.expect("unwrap_signed_event failed");
let parsed: Value = serde_json::from_str(&result).expect("should be valid JSON");
assert_eq!(parsed["verified"], false);
assert_eq!(parsed["data"]["type"], "heartbeat");
}
#[test]
fn protocol_unwrap_signed_event_invalid_event_json() {
let wrapper = protocol_wrapper();
let result = wrapper.unwrap_signed_event("not json", "{}");
assert!(result.is_err());
assert_eq!(result.unwrap_err().kind, ErrorKind::SerializationFailed);
}
#[test]
fn protocol_unwrap_signed_event_invalid_keys_json() {
let wrapper = protocol_wrapper();
let result = wrapper.unwrap_signed_event(r#"{"type":"test"}"#, "not json");
assert!(result.is_err());
assert_eq!(result.unwrap_err().kind, ErrorKind::SerializationFailed);
}
#[cfg(feature = "attestation")]
mod attestation_tests {
use super::*;
fn attestation_wrapper() -> AgentWrapper {
let wrapper = AgentWrapper::new();
wrapper.ephemeral(Some("ed25519")).unwrap();
wrapper
}
fn basic_attestation_params() -> String {
json!({
"subject": {
"type": "artifact",
"id": "test-artifact-001",
"digests": { "sha256": "abc123" }
},
"claims": [{
"name": "reviewed",
"value": true,
"confidence": 0.95,
"assuranceLevel": "verified"
}]
})
.to_string()
}
#[test]
fn binding_create_attestation_json() {
let wrapper = attestation_wrapper();
let result = wrapper.create_attestation(&basic_attestation_params());
assert!(
result.is_ok(),
"create_attestation should succeed: {:?}",
result.err()
);
let json_str = result.unwrap();
let doc: Value = serde_json::from_str(&json_str).unwrap();
assert!(
doc.get("attestation").is_some(),
"returned JSON should contain 'attestation' key"
);
assert!(
doc.get("jacsSignature").is_some(),
"returned JSON should be signed"
);
}
#[test]
fn binding_verify_attestation_json() {
let wrapper = attestation_wrapper();
let att_json = wrapper
.create_attestation(&basic_attestation_params())
.unwrap();
let doc: Value = serde_json::from_str(&att_json).unwrap();
let key = format!(
"{}:{}",
doc["jacsId"].as_str().unwrap(),
doc["jacsVersion"].as_str().unwrap()
);
let result = wrapper.verify_attestation(&key);
assert!(
result.is_ok(),
"verify_attestation should succeed: {:?}",
result.err()
);
let result_json = result.unwrap();
let result_value: Value = serde_json::from_str(&result_json).unwrap();
assert_eq!(
result_value["valid"], true,
"attestation should verify as valid"
);
}
#[test]
fn binding_verify_attestation_full_json() {
let wrapper = attestation_wrapper();
let att_json = wrapper
.create_attestation(&basic_attestation_params())
.unwrap();
let doc: Value = serde_json::from_str(&att_json).unwrap();
let key = format!(
"{}:{}",
doc["jacsId"].as_str().unwrap(),
doc["jacsVersion"].as_str().unwrap()
);
let result = wrapper.verify_attestation_full(&key);
assert!(
result.is_ok(),
"verify_attestation_full should succeed: {:?}",
result.err()
);
let result_json = result.unwrap();
let result_value: Value = serde_json::from_str(&result_json).unwrap();
assert_eq!(
result_value["valid"], true,
"full attestation should verify as valid"
);
assert!(
result_value.get("evidence").is_some(),
"full verification result should contain 'evidence' array"
);
}
#[test]
fn binding_lift_to_attestation_json() {
let wrapper = attestation_wrapper();
let doc_json = json!({"title": "Test Document", "content": "Some content"}).to_string();
let signed = wrapper
.create_document(&doc_json, None, None, true, None, None)
.unwrap();
let claims_json = json!([{
"name": "reviewed",
"value": true
}])
.to_string();
let result = wrapper.lift_to_attestation(&signed, &claims_json);
assert!(
result.is_ok(),
"lift_to_attestation should succeed: {:?}",
result.err()
);
let att_json = result.unwrap();
let doc: Value = serde_json::from_str(&att_json).unwrap();
assert!(
doc.get("attestation").is_some(),
"lifted result should contain 'attestation' key"
);
assert!(
doc.get("jacsSignature").is_some(),
"lifted result should be signed"
);
}
#[test]
fn binding_create_attestation_error_on_bad_json() {
let wrapper = attestation_wrapper();
let result = wrapper.create_attestation("not valid json {{{");
assert!(result.is_err(), "bad JSON should error");
assert_eq!(
result.unwrap_err().kind,
ErrorKind::SerializationFailed,
"should be SerializationFailed error"
);
}
#[test]
fn binding_create_attestation_error_on_missing_fields() {
let wrapper = attestation_wrapper();
let params = json!({
"claims": [{"name": "test", "value": true}]
})
.to_string();
let result = wrapper.create_attestation(¶ms);
assert!(result.is_err(), "missing subject should error");
assert_eq!(
result.unwrap_err().kind,
ErrorKind::Validation,
"should be Validation error"
);
}
#[test]
fn binding_export_attestation_dsse() {
let wrapper = attestation_wrapper();
let att_json = wrapper
.create_attestation(&basic_attestation_params())
.unwrap();
let result = wrapper.export_attestation_dsse(&att_json);
assert!(
result.is_ok(),
"export_attestation_dsse should succeed: {:?}",
result.err()
);
let dsse_json = result.unwrap();
let envelope: Value = serde_json::from_str(&dsse_json).unwrap();
assert_eq!(
envelope["payloadType"].as_str().unwrap(),
"application/vnd.in-toto+json"
);
}
}
}