use crate::agent::document::DocumentTraits;
use crate::agent::Agent;
use crate::error::JacsError;
use crate::mime::mime_from_extension;
use crate::schema::utils::{check_document_size, ValueExt};
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
use std::fs;
use std::path::Path;
use std::sync::Mutex;
use tracing::{debug, info};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentInfo {
pub agent_id: String,
pub name: String,
pub public_key_path: String,
pub config_path: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SignedDocument {
pub raw: String,
pub document_id: String,
pub agent_id: String,
pub timestamp: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VerificationResult {
pub valid: bool,
pub data: Value,
pub signer_id: String,
pub signer_name: Option<String>,
pub timestamp: String,
pub attachments: Vec<Attachment>,
pub errors: Vec<String>,
}
impl VerificationResult {
#[must_use]
pub fn failure(error: String) -> Self {
Self {
valid: false,
data: json!(null),
signer_id: String::new(),
signer_name: None,
timestamp: String::new(),
attachments: vec![],
errors: vec![error],
}
}
#[must_use]
pub fn success(data: Value, signer_id: String, timestamp: String) -> Self {
Self {
valid: true,
data,
signer_id,
signer_name: None,
timestamp,
attachments: vec![],
errors: vec![],
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Attachment {
pub filename: String,
pub mime_type: String,
#[serde(with = "base64_bytes")]
pub content: Vec<u8>,
pub hash: String,
pub embedded: bool,
}
mod base64_bytes {
use base64::{Engine as _, engine::general_purpose::STANDARD};
use serde::{Deserialize, Deserializer, Serializer};
pub fn serialize<S>(bytes: &Vec<u8>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&STANDARD.encode(bytes))
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
STANDARD.decode(&s).map_err(serde::de::Error::custom)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SignerStatus {
pub agent_id: String,
pub signed: bool,
pub signed_at: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgreementStatus {
pub complete: bool,
pub signers: Vec<SignerStatus>,
pub pending: Vec<String>,
}
pub struct SimpleAgent {
agent: Mutex<Agent>,
config_path: Option<String>,
}
impl SimpleAgent {
#[must_use = "agent creation result must be checked for errors"]
pub fn create(
name: &str,
purpose: Option<&str>,
key_algorithm: Option<&str>,
) -> Result<(Self, AgentInfo), JacsError> {
let algorithm = key_algorithm.unwrap_or("ed25519");
info!("Creating new agent '{}' with algorithm '{}'", name, algorithm);
let keys_dir = Path::new("./jacs_keys");
let data_dir = Path::new("./jacs_data");
fs::create_dir_all(keys_dir).map_err(|e| JacsError::DirectoryCreateFailed {
path: keys_dir.to_string_lossy().to_string(),
reason: e.to_string(),
})?;
fs::create_dir_all(data_dir).map_err(|e| JacsError::DirectoryCreateFailed {
path: data_dir.to_string_lossy().to_string(),
reason: e.to_string(),
})?;
let agent_type = "ai";
let description = purpose.unwrap_or("JACS agent");
let agent_json = json!({
"jacsAgentType": agent_type,
"name": name,
"description": description,
});
let mut agent = crate::get_empty_agent();
let instance = agent
.create_agent_and_load(&agent_json.to_string(), true, Some(algorithm))
.map_err(|e| JacsError::Internal {
message: format!("Failed to create agent: {}", e),
})?;
let agent_id = instance["jacsId"]
.as_str()
.unwrap_or("unknown")
.to_string();
let version = instance["jacsVersion"]
.as_str()
.unwrap_or("unknown")
.to_string();
let lookup_id = format!("{}:{}", agent_id, version);
agent.save().map_err(|e| JacsError::Internal {
message: format!("Failed to save agent: {}", e),
})?;
let config_json = json!({
"$schema": "https://hai.ai/schemas/jacs.config.schema.json",
"jacs_agent_id_and_version": lookup_id,
"jacs_data_directory": "./jacs_data",
"jacs_key_directory": "./jacs_keys",
"jacs_agent_key_algorithm": algorithm,
"jacs_default_storage": "fs"
});
let config_path = "./jacs.config.json";
let config_str = serde_json::to_string_pretty(&config_json).map_err(|e| JacsError::Internal {
message: format!("Failed to serialize config: {}", e),
})?;
fs::write(config_path, config_str).map_err(|e| JacsError::Internal {
message: format!("Failed to write config: {}", e),
})?;
info!("Agent '{}' created successfully with ID {}", name, agent_id);
let info = AgentInfo {
agent_id,
name: name.to_string(),
public_key_path: "./jacs_keys/jacs.public.pem".to_string(),
config_path: config_path.to_string(),
};
Ok((
Self {
agent: Mutex::new(agent),
config_path: Some(config_path.to_string()),
},
info,
))
}
#[must_use = "agent loading result must be checked for errors"]
pub fn load(config_path: Option<&str>) -> Result<Self, JacsError> {
let path = config_path.unwrap_or("./jacs.config.json");
debug!("Loading agent from config: {}", path);
if !Path::new(path).exists() {
return Err(JacsError::ConfigNotFound {
path: path.to_string(),
});
}
let mut agent = crate::get_empty_agent();
agent.load_by_config(path.to_string()).map_err(|e| {
JacsError::ConfigInvalid {
field: "config".to_string(),
reason: e.to_string(),
}
})?;
info!("Agent loaded successfully from {}", path);
Ok(Self {
agent: Mutex::new(agent),
config_path: Some(path.to_string()),
})
}
#[must_use = "self-verification result must be checked"]
pub fn verify_self(&self) -> Result<VerificationResult, JacsError> {
let mut agent = self.agent.lock().map_err(|e| JacsError::Internal {
message: format!("Failed to acquire agent lock: {}", e),
})?;
let sig_result = agent.verify_self_signature();
let hash_result = agent.verify_self_hash();
let mut errors = Vec::new();
if let Err(e) = sig_result {
errors.push(format!("Signature verification failed: {}", e));
}
if let Err(e) = hash_result {
errors.push(format!("Hash verification failed: {}", e));
}
let valid = errors.is_empty();
let agent_value = agent.get_value().cloned().unwrap_or(json!({}));
let agent_id = agent_value.get_str_or("jacsId", "");
let agent_name = agent_value.get_str("name");
let timestamp = agent_value.get_str_or("jacsVersionDate", "");
Ok(VerificationResult {
valid,
data: agent_value,
signer_id: agent_id.clone(),
signer_name: agent_name,
timestamp,
attachments: vec![],
errors,
})
}
#[must_use = "updated agent JSON must be used or stored"]
pub fn update_agent(&self, new_agent_data: &str) -> Result<String, JacsError> {
check_document_size(new_agent_data)?;
let mut agent = self.agent.lock().map_err(|e| JacsError::Internal {
message: format!("Failed to acquire agent lock: {}", e),
})?;
agent.update_self(new_agent_data).map_err(|e| JacsError::Internal {
message: format!("Failed to update agent: {}", e),
})
}
#[must_use = "updated document must be used or stored"]
pub fn update_document(
&self,
document_id: &str,
new_data: &str,
attachments: Option<Vec<String>>,
embed: Option<bool>,
) -> Result<SignedDocument, JacsError> {
check_document_size(new_data)?;
let mut agent = self.agent.lock().map_err(|e| JacsError::Internal {
message: format!("Failed to acquire agent lock: {}", e),
})?;
let jacs_doc = agent
.update_document(document_id, new_data, attachments, embed)
.map_err(|e| JacsError::Internal {
message: format!("Failed to update document: {}", e),
})?;
let raw = serde_json::to_string(&jacs_doc.value).map_err(|e| JacsError::Internal {
message: format!("Failed to serialize document: {}", e),
})?;
let timestamp = jacs_doc.value.get_path_str_or(&["jacsSignature", "date"], "");
let agent_id = jacs_doc.value.get_path_str_or(&["jacsSignature", "agentID"], "");
Ok(SignedDocument {
raw,
document_id: jacs_doc.id,
agent_id,
timestamp,
})
}
#[must_use = "signed document must be used or stored"]
pub fn sign_message(&self, data: &Value) -> Result<SignedDocument, JacsError> {
debug!("sign_message() called");
let doc_content = json!({
"jacsType": "message",
"jacsLevel": "raw",
"content": data
});
let doc_string = doc_content.to_string();
check_document_size(&doc_string)?;
let mut agent = self.agent.lock().map_err(|e| JacsError::Internal {
message: format!("Failed to acquire agent lock: {}", e),
})?;
let jacs_doc = agent
.create_document_and_load(&doc_string, None, None)
.map_err(|e| JacsError::SigningFailed {
reason: format!(
"{}. Ensure the agent is properly initialized with load() or create() and has valid keys.",
e
),
})?;
let raw = serde_json::to_string(&jacs_doc.value).map_err(|e| JacsError::Internal {
message: format!("Failed to serialize document: {}", e),
})?;
let timestamp = jacs_doc.value.get_path_str_or(&["jacsSignature", "date"], "");
let agent_id = jacs_doc.value.get_path_str_or(&["jacsSignature", "agentID"], "");
info!("Message signed: document_id={}", jacs_doc.id);
Ok(SignedDocument {
raw,
document_id: jacs_doc.id,
agent_id,
timestamp,
})
}
#[must_use = "signed document must be used or stored"]
pub fn sign_file(&self, file_path: &str, embed: bool) -> Result<SignedDocument, JacsError> {
if !Path::new(file_path).exists() {
return Err(JacsError::FileNotFound {
path: file_path.to_string(),
});
}
let mime_type = mime_from_extension(file_path);
let filename = Path::new(file_path)
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("file");
let mut agent = self.agent.lock().map_err(|e| JacsError::Internal {
message: format!("Failed to acquire agent lock: {}", e),
})?;
let doc_content = json!({
"jacsType": "file",
"jacsLevel": "raw",
"filename": filename,
"mimetype": mime_type
});
let attachment = vec![file_path.to_string()];
let jacs_doc = agent
.create_document_and_load(&doc_content.to_string(), Some(attachment), Some(embed))
.map_err(|e| JacsError::SigningFailed {
reason: format!(
"File signing failed for '{}': {}. Verify the file exists and the agent has valid keys.",
file_path, e
),
})?;
let raw = serde_json::to_string(&jacs_doc.value).map_err(|e| JacsError::Internal {
message: format!("Failed to serialize document: {}", e),
})?;
let timestamp = jacs_doc.value.get_path_str_or(&["jacsSignature", "date"], "");
let agent_id = jacs_doc.value.get_path_str_or(&["jacsSignature", "agentID"], "");
Ok(SignedDocument {
raw,
document_id: jacs_doc.id,
agent_id,
timestamp,
})
}
pub fn sign_messages_batch(&self, messages: &[&Value]) -> Result<Vec<SignedDocument>, JacsError> {
use crate::agent::document::DocumentTraits;
use tracing::info;
if messages.is_empty() {
return Ok(Vec::new());
}
info!(
batch_size = messages.len(),
"Signing batch of messages"
);
let doc_strings: Vec<String> = messages
.iter()
.map(|data| {
let doc_content = json!({
"jacsType": "message",
"jacsLevel": "raw",
"content": data
});
doc_content.to_string()
})
.collect();
for doc_str in &doc_strings {
check_document_size(doc_str)?;
}
let mut agent = self.agent.lock().map_err(|e| JacsError::Internal {
message: format!("Failed to acquire agent lock: {}", e),
})?;
let doc_refs: Vec<&str> = doc_strings.iter().map(|s| s.as_str()).collect();
let jacs_docs = agent
.create_documents_batch(&doc_refs)
.map_err(|e| JacsError::SigningFailed {
reason: format!(
"Batch signing failed: {}. Ensure the agent is properly initialized with load() or create() and has valid keys.",
e
),
})?;
let mut results = Vec::with_capacity(jacs_docs.len());
for jacs_doc in jacs_docs {
let raw = serde_json::to_string(&jacs_doc.value).map_err(|e| JacsError::Internal {
message: format!("Failed to serialize document: {}", e),
})?;
let timestamp = jacs_doc.value.get_path_str_or(&["jacsSignature", "date"], "");
let agent_id = jacs_doc.value.get_path_str_or(&["jacsSignature", "agentID"], "");
results.push(SignedDocument {
raw,
document_id: jacs_doc.id,
agent_id,
timestamp,
});
}
info!(
batch_size = results.len(),
"Batch message signing completed successfully"
);
Ok(results)
}
#[must_use = "verification result must be checked"]
pub fn verify(&self, signed_document: &str) -> Result<VerificationResult, JacsError> {
debug!("verify() called");
check_document_size(signed_document)?;
let _: Value = serde_json::from_str(signed_document).map_err(|e| {
JacsError::DocumentMalformed {
field: "json".to_string(),
reason: e.to_string(),
}
})?;
let mut agent = self.agent.lock().map_err(|e| JacsError::Internal {
message: format!("Failed to acquire agent lock: {}", e),
})?;
let jacs_doc = agent.load_document(signed_document).map_err(|e| {
JacsError::DocumentMalformed {
field: "document".to_string(),
reason: e.to_string(),
}
})?;
let document_key = jacs_doc.getkey();
let verification_result = agent.verify_document_signature(&document_key, None, None, None, None);
let mut errors = Vec::new();
if let Err(e) = verification_result {
errors.push(e.to_string());
}
if let Err(e) = agent.verify_hash(&jacs_doc.value) {
errors.push(format!("Hash verification failed: {}", e));
}
let valid = errors.is_empty();
let signer_id = jacs_doc.value.get_path_str_or(&["jacsSignature", "agentID"], "");
let timestamp = jacs_doc.value.get_path_str_or(&["jacsSignature", "date"], "");
info!("Document verified: valid={}, signer={}", valid, signer_id);
let data = if let Some(content) = jacs_doc.value.get("content") {
content.clone()
} else {
jacs_doc.value.clone()
};
let attachments = extract_attachments(&jacs_doc.value);
Ok(VerificationResult {
valid,
data,
signer_id,
signer_name: None, timestamp,
attachments,
errors,
})
}
#[must_use = "exported agent data must be used"]
pub fn export_agent(&self) -> Result<String, JacsError> {
let agent = self.agent.lock().map_err(|e| JacsError::Internal {
message: format!("Failed to acquire agent lock: {}", e),
})?;
let value = agent.get_value().cloned().ok_or(JacsError::AgentNotLoaded)?;
serde_json::to_string_pretty(&value).map_err(|e| JacsError::Internal {
message: format!("Failed to serialize agent: {}", e),
})
}
#[must_use = "public key data must be used"]
pub fn get_public_key_pem(&self) -> Result<String, JacsError> {
let key_path = "./jacs_keys/jacs.public.pem";
fs::read_to_string(key_path).map_err(|e| {
let reason = match e.kind() {
std::io::ErrorKind::NotFound => {
"file does not exist. Run agent creation to generate keys first.".to_string()
}
std::io::ErrorKind::PermissionDenied => {
"permission denied. Check that the key file is readable.".to_string()
}
_ => e.to_string(),
};
JacsError::FileReadFailed {
path: key_path.to_string(),
reason,
}
})
}
pub fn config_path(&self) -> Option<&str> {
self.config_path.as_deref()
}
#[must_use]
pub fn verify_batch(&self, documents: &[&str]) -> Vec<VerificationResult> {
documents
.iter()
.map(|doc| {
match self.verify(doc) {
Ok(result) => result,
Err(e) => VerificationResult::failure(e.to_string()),
}
})
.collect()
}
#[must_use = "agreement document must be used or stored"]
pub fn create_agreement(
&self,
document: &str,
agent_ids: &[String],
question: Option<&str>,
context: Option<&str>,
) -> Result<SignedDocument, JacsError> {
use crate::agent::agreement::Agreement;
debug!("create_agreement() called with {} signers", agent_ids.len());
check_document_size(document)?;
let mut agent = self.agent.lock().map_err(|e| JacsError::Internal {
message: format!("Failed to acquire agent lock: {}", e),
})?;
let jacs_doc = agent
.create_document_and_load(document, None, None)
.map_err(|e| JacsError::SigningFailed {
reason: format!("Failed to create base document: {}", e),
})?;
let agreement_doc = agent
.create_agreement(&jacs_doc.getkey(), agent_ids, question, context, None)
.map_err(|e| JacsError::Internal {
message: format!("Failed to create agreement: {}", e),
})?;
let raw = serde_json::to_string(&agreement_doc.value).map_err(|e| JacsError::Internal {
message: format!("Failed to serialize agreement: {}", e),
})?;
let timestamp = agreement_doc.value.get_path_str_or(&["jacsSignature", "date"], "");
let agent_id = agreement_doc.value.get_path_str_or(&["jacsSignature", "agentID"], "");
info!("Agreement created: document_id={}", agreement_doc.id);
Ok(SignedDocument {
raw,
document_id: agreement_doc.id,
agent_id,
timestamp,
})
}
#[must_use = "signed agreement must be used or stored"]
pub fn sign_agreement(&self, document: &str) -> Result<SignedDocument, JacsError> {
use crate::agent::agreement::Agreement;
check_document_size(document)?;
let mut agent = self.agent.lock().map_err(|e| JacsError::Internal {
message: format!("Failed to acquire agent lock: {}", e),
})?;
let jacs_doc = agent.load_document(document).map_err(|e| {
JacsError::DocumentMalformed {
field: "document".to_string(),
reason: e.to_string(),
}
})?;
let signed_doc = agent
.sign_agreement(&jacs_doc.getkey(), None)
.map_err(|e| JacsError::SigningFailed {
reason: format!("Failed to sign agreement: {}", e),
})?;
let raw = serde_json::to_string(&signed_doc.value).map_err(|e| JacsError::Internal {
message: format!("Failed to serialize signed agreement: {}", e),
})?;
let timestamp = signed_doc.value.get_path_str_or(&["jacsSignature", "date"], "");
let agent_id = signed_doc.value.get_path_str_or(&["jacsSignature", "agentID"], "");
Ok(SignedDocument {
raw,
document_id: signed_doc.id,
agent_id,
timestamp,
})
}
#[must_use = "agreement status must be checked"]
pub fn check_agreement(&self, document: &str) -> Result<AgreementStatus, JacsError> {
check_document_size(document)?;
let mut agent = self.agent.lock().map_err(|e| JacsError::Internal {
message: format!("Failed to acquire agent lock: {}", e),
})?;
let jacs_doc = agent.load_document(document).map_err(|e| {
JacsError::DocumentMalformed {
field: "document".to_string(),
reason: e.to_string(),
}
})?;
let unsigned = jacs_doc.agreement_unsigned_agents(None).map_err(|e| {
JacsError::Internal {
message: format!("Failed to check unsigned agents: {}", e),
}
})?;
let all_agents = jacs_doc.agreement_requested_agents(None).map_err(|e| {
JacsError::Internal {
message: format!("Failed to get agreement agents: {}", e),
}
})?;
let mut signers = Vec::new();
let unsigned_set: std::collections::HashSet<&String> = unsigned.iter().collect();
for agent_id in &all_agents {
let signed = !unsigned_set.contains(agent_id);
signers.push(SignerStatus {
agent_id: agent_id.clone(),
signed,
signed_at: if signed {
Some(jacs_doc.value.get_path_str_or(&["jacsSignature", "date"], "").to_string())
} else {
None
},
});
}
Ok(AgreementStatus {
complete: unsigned.is_empty(),
signers,
pending: unsigned,
})
}
}
use std::cell::RefCell;
thread_local! {
static THREAD_AGENT: RefCell<Option<SimpleAgent>> = const { RefCell::new(None) };
}
#[deprecated(since = "0.3.0", note = "Use SimpleAgent::create() instead")]
pub fn create(
name: &str,
purpose: Option<&str>,
key_algorithm: Option<&str>,
) -> Result<AgentInfo, JacsError> {
let (agent, info) = SimpleAgent::create(name, purpose, key_algorithm)?;
THREAD_AGENT.with(|cell| {
*cell.borrow_mut() = Some(agent);
});
Ok(info)
}
#[deprecated(since = "0.3.0", note = "Use SimpleAgent::load() instead")]
pub fn load(config_path: Option<&str>) -> Result<(), JacsError> {
let agent = SimpleAgent::load(config_path)?;
THREAD_AGENT.with(|cell| {
*cell.borrow_mut() = Some(agent);
});
Ok(())
}
fn with_thread_agent<F, T>(f: F) -> Result<T, JacsError>
where
F: FnOnce(&SimpleAgent) -> Result<T, JacsError>,
{
THREAD_AGENT.with(|cell| {
let borrow = cell.borrow();
let agent = borrow.as_ref().ok_or(JacsError::AgentNotLoaded)?;
f(agent)
})
}
#[deprecated(since = "0.3.0", note = "Use SimpleAgent::verify_self() instead")]
pub fn verify_self() -> Result<VerificationResult, JacsError> {
with_thread_agent(|agent| agent.verify_self())
}
#[deprecated(since = "0.3.0", note = "Use SimpleAgent::update_agent() instead")]
pub fn update_agent(new_agent_data: &str) -> Result<String, JacsError> {
with_thread_agent(|agent| agent.update_agent(new_agent_data))
}
#[deprecated(since = "0.3.0", note = "Use SimpleAgent::update_document() instead")]
pub fn update_document(
document_id: &str,
new_data: &str,
attachments: Option<Vec<String>>,
embed: Option<bool>,
) -> Result<SignedDocument, JacsError> {
with_thread_agent(|agent| agent.update_document(document_id, new_data, attachments, embed))
}
#[deprecated(since = "0.3.0", note = "Use SimpleAgent::sign_message() instead")]
pub fn sign_message(data: &Value) -> Result<SignedDocument, JacsError> {
with_thread_agent(|agent| agent.sign_message(data))
}
#[deprecated(since = "0.3.0", note = "Use SimpleAgent::sign_file() instead")]
pub fn sign_file(file_path: &str, embed: bool) -> Result<SignedDocument, JacsError> {
with_thread_agent(|agent| agent.sign_file(file_path, embed))
}
#[deprecated(since = "0.3.0", note = "Use SimpleAgent::verify() instead")]
pub fn verify(signed_document: &str) -> Result<VerificationResult, JacsError> {
with_thread_agent(|agent| agent.verify(signed_document))
}
#[deprecated(since = "0.3.0", note = "Use SimpleAgent::export_agent() instead")]
pub fn export_agent() -> Result<String, JacsError> {
with_thread_agent(|agent| agent.export_agent())
}
#[deprecated(since = "0.3.0", note = "Use SimpleAgent::get_public_key_pem() instead")]
pub fn get_public_key_pem() -> Result<String, JacsError> {
with_thread_agent(|agent| agent.get_public_key_pem())
}
#[deprecated(since = "0.3.0", note = "Use SimpleAgent::create_agreement() instead")]
pub fn create_agreement(
document: &str,
agent_ids: &[String],
question: Option<&str>,
context: Option<&str>,
) -> Result<SignedDocument, JacsError> {
with_thread_agent(|agent| agent.create_agreement(document, agent_ids, question, context))
}
#[deprecated(since = "0.3.0", note = "Use SimpleAgent::sign_agreement() instead")]
pub fn sign_agreement(document: &str) -> Result<SignedDocument, JacsError> {
with_thread_agent(|agent| agent.sign_agreement(document))
}
#[deprecated(since = "0.3.0", note = "Use SimpleAgent::check_agreement() instead")]
pub fn check_agreement(document: &str) -> Result<AgreementStatus, JacsError> {
with_thread_agent(|agent| agent.check_agreement(document))
}
fn extract_attachments(doc: &Value) -> Vec<Attachment> {
let mut attachments = Vec::new();
if let Some(files) = doc.get("jacsFiles").and_then(|f| f.as_array()) {
for file in files {
let filename = file["path"]
.as_str()
.unwrap_or("unknown")
.to_string();
let mime_type = file["mimetype"]
.as_str()
.unwrap_or("application/octet-stream")
.to_string();
let hash = file["sha256"].as_str().unwrap_or("").to_string();
let embedded = file["embed"].as_bool().unwrap_or(false);
let content = if embedded {
if let Some(contents_b64) = file["contents"].as_str() {
use base64::{Engine as _, engine::general_purpose::STANDARD};
STANDARD.decode(contents_b64).unwrap_or_default()
} else {
Vec::new()
}
} else {
Vec::new()
};
attachments.push(Attachment {
filename,
mime_type,
content,
hash,
embedded,
});
}
}
attachments
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_agent_info_serialization() {
let info = AgentInfo {
agent_id: "test-id".to_string(),
name: "Test Agent".to_string(),
public_key_path: "./keys/public.pem".to_string(),
config_path: "./config.json".to_string(),
};
let json = serde_json::to_string(&info).unwrap();
assert!(json.contains("test-id"));
assert!(json.contains("Test Agent"));
}
#[test]
fn test_verification_result_serialization() {
let result = VerificationResult {
valid: true,
data: json!({"test": "data"}),
signer_id: "agent-123".to_string(),
signer_name: Some("Test Agent".to_string()),
timestamp: "2024-01-01T00:00:00Z".to_string(),
attachments: vec![],
errors: vec![],
};
let json = serde_json::to_string(&result).unwrap();
assert!(json.contains("\"valid\":true"));
assert!(json.contains("agent-123"));
}
#[test]
fn test_signed_document_serialization() {
let doc = SignedDocument {
raw: r#"{"test":"doc"}"#.to_string(),
document_id: "doc-456".to_string(),
agent_id: "agent-789".to_string(),
timestamp: "2024-01-01T12:00:00Z".to_string(),
};
let json = serde_json::to_string(&doc).unwrap();
assert!(json.contains("doc-456"));
assert!(json.contains("agent-789"));
}
#[test]
fn test_attachment_serialization() {
let att = Attachment {
filename: "test.txt".to_string(),
mime_type: "text/plain".to_string(),
content: b"hello world".to_vec(),
hash: "abc123".to_string(),
embedded: true,
};
let json = serde_json::to_string(&att).unwrap();
assert!(json.contains("test.txt"));
assert!(json.contains("text/plain"));
assert!(json.contains("abc123"));
}
#[test]
fn test_thread_agent_not_loaded() {
THREAD_AGENT.with(|cell| {
*cell.borrow_mut() = None;
});
#[allow(deprecated)]
let result = verify_self();
assert!(result.is_err());
match result {
Err(JacsError::AgentNotLoaded) => (),
_ => panic!("Expected AgentNotLoaded error"),
}
}
#[test]
fn test_simple_agent_load_missing_config() {
let result = SimpleAgent::load(Some("/nonexistent/path/config.json"));
assert!(result.is_err());
match result {
Err(JacsError::ConfigNotFound { path }) => {
assert!(path.contains("nonexistent"));
}
_ => panic!("Expected ConfigNotFound error"),
}
}
#[test]
#[allow(deprecated)]
fn test_deprecated_load_missing_config() {
let result = load(Some("/nonexistent/path/config.json"));
assert!(result.is_err());
match result {
Err(JacsError::ConfigNotFound { path }) => {
assert!(path.contains("nonexistent"));
}
_ => panic!("Expected ConfigNotFound error"),
}
}
#[test]
#[allow(deprecated)]
fn test_sign_file_missing_file() {
THREAD_AGENT.with(|cell| {
*cell.borrow_mut() = None;
});
let result = sign_file("/nonexistent/file.txt", false);
assert!(result.is_err());
}
#[test]
fn test_verification_result_with_errors() {
let result = VerificationResult {
valid: false,
data: json!(null),
signer_id: "".to_string(),
signer_name: None,
timestamp: "".to_string(),
attachments: vec![],
errors: vec![
"Signature invalid".to_string(),
"Hash mismatch".to_string(),
],
};
assert!(!result.valid);
assert_eq!(result.errors.len(), 2);
assert!(result.errors[0].contains("Signature"));
assert!(result.errors[1].contains("Hash"));
}
#[test]
fn test_extract_attachments_empty() {
let doc = json!({});
let attachments = extract_attachments(&doc);
assert!(attachments.is_empty());
}
#[test]
fn test_extract_attachments_with_files() {
let doc = json!({
"jacsFiles": [
{
"path": "document.pdf",
"mimetype": "application/pdf",
"sha256": "abcdef123456",
"embed": false
},
{
"path": "image.png",
"mimetype": "image/png",
"sha256": "fedcba654321",
"embed": true,
"contents": "SGVsbG8gV29ybGQ="
}
]
});
let attachments = extract_attachments(&doc);
assert_eq!(attachments.len(), 2);
assert_eq!(attachments[0].filename, "document.pdf");
assert_eq!(attachments[0].mime_type, "application/pdf");
assert!(!attachments[0].embedded);
assert!(attachments[0].content.is_empty());
assert_eq!(attachments[1].filename, "image.png");
assert_eq!(attachments[1].mime_type, "image/png");
assert!(attachments[1].embedded);
assert!(!attachments[1].content.is_empty());
}
#[test]
#[allow(deprecated)]
fn test_get_public_key_pem_not_found() {
THREAD_AGENT.with(|cell| {
*cell.borrow_mut() = None;
});
let result = get_public_key_pem();
assert!(result.is_err());
}
#[test]
fn test_simple_agent_struct_has_config_path() {
let result = SimpleAgent::load(Some("./nonexistent.json"));
assert!(result.is_err());
}
#[test]
fn test_verification_result_failure_constructor() {
let result = VerificationResult::failure("Test error message".to_string());
assert!(!result.valid);
assert_eq!(result.errors.len(), 1);
assert!(result.errors[0].contains("Test error message"));
assert_eq!(result.signer_id, "");
assert!(result.signer_name.is_none());
}
#[test]
fn test_verification_result_success_constructor() {
let data = json!({"message": "hello"});
let signer_id = "agent-123".to_string();
let timestamp = "2024-01-15T10:30:00Z".to_string();
let result = VerificationResult::success(data.clone(), signer_id.clone(), timestamp.clone());
assert!(result.valid);
assert_eq!(result.data, data);
assert_eq!(result.signer_id, signer_id);
assert!(result.signer_name.is_none());
assert_eq!(result.timestamp, timestamp);
assert!(result.attachments.is_empty());
assert!(result.errors.is_empty());
}
#[test]
fn test_verification_result_failure_has_null_data() {
let result = VerificationResult::failure("error".to_string());
assert_eq!(result.data, json!(null));
assert!(result.timestamp.is_empty());
assert!(result.attachments.is_empty());
}
}