use crate::agent::Agent;
use crate::agent::boilerplate::BoilerPlate;
use crate::agent::document::DocumentTraits;
use crate::create_minimal_blank_agent;
use crate::error::JacsError;
use crate::mime::mime_from_extension;
use crate::schema::utils::{ValueExt, check_document_size};
use serde_json::{Value, json};
use std::fs;
use std::path::{Component, Path, PathBuf};
use std::sync::Mutex;
use tracing::{debug, info, warn};
use super::types::*;
pub(crate) const DEFAULT_PRIVATE_KEY_FILENAME: &str = "jacs.private.pem.enc";
pub(crate) const DEFAULT_PUBLIC_KEY_FILENAME: &str = "jacs.public.pem";
pub(crate) fn build_agent_document(
agent_type: &str,
name: &str,
description: &str,
) -> Result<Value, JacsError> {
let template =
create_minimal_blank_agent(agent_type.to_string(), None, None, None).map_err(|e| {
JacsError::Internal {
message: format!("Failed to create minimal agent template: {}", e),
}
})?;
let mut agent_json: Value =
serde_json::from_str(&template).map_err(|e| JacsError::Internal {
message: format!("Failed to parse minimal agent template JSON: {}", e),
})?;
let obj = agent_json
.as_object_mut()
.ok_or_else(|| JacsError::Internal {
message: "Generated minimal agent template is not a JSON object".to_string(),
})?;
obj.insert("name".to_string(), json!(name));
obj.insert("description".to_string(), json!(description));
Ok(agent_json)
}
pub(crate) fn write_key_directory_ignore_files(key_dir: &Path) {
let ignore_content = "# JACS private key material — do NOT commit or ship\n\
*.pem\n\
*.pem.enc\n\
.jacs_password\n\
*.key\n\
*.key.enc\n";
let gitignore_path = key_dir.join(".gitignore");
if !gitignore_path.exists() {
if let Err(e) = std::fs::write(&gitignore_path, ignore_content) {
warn!("Could not write {}: {}", gitignore_path.display(), e);
}
}
let dockerignore_path = key_dir.join(".dockerignore");
if !dockerignore_path.exists() {
if let Err(e) = std::fs::write(&dockerignore_path, ignore_content) {
warn!("Could not write {}: {}", dockerignore_path.display(), e);
}
}
}
pub(crate) fn resolve_strict(explicit: Option<bool>) -> bool {
if let Some(s) = explicit {
return s;
}
crate::storage::jenv::get_env_var("JACS_STRICT_MODE", false)
.ok()
.flatten()
.map(|v| v.eq_ignore_ascii_case("true") || v == "1")
.unwrap_or(false)
}
fn normalize_path(path: &Path) -> PathBuf {
let mut normalized = PathBuf::new();
for component in path.components() {
match component {
Component::CurDir => {}
Component::ParentDir => {
normalized.pop();
}
other => normalized.push(other.as_os_str()),
}
}
normalized
}
fn resolve_config_relative_path(config_path: &Path, candidate: &str) -> PathBuf {
let candidate_path = Path::new(candidate);
if candidate_path.is_absolute() {
normalize_path(candidate_path)
} else {
let config_dir = config_path
.parent()
.filter(|path| !path.as_os_str().is_empty())
.unwrap_or_else(|| Path::new("."));
normalize_path(&config_dir.join(candidate_path))
}
}
pub fn build_loaded_agent_info(
agent: &crate::agent::Agent,
config_path: &str,
) -> Result<AgentInfo, JacsError> {
let resolved_config_path = if Path::new(config_path).is_absolute() {
normalize_path(Path::new(config_path))
} else {
normalize_path(&std::env::current_dir()?.join(config_path))
};
let agent_value = agent
.get_value()
.cloned()
.ok_or(JacsError::AgentNotLoaded)?;
let config = agent.config.as_ref();
let key_directory = resolve_config_relative_path(
&resolved_config_path,
config
.and_then(|cfg| cfg.jacs_key_directory().as_deref())
.unwrap_or("./jacs_keys"),
);
let data_directory = resolve_config_relative_path(
&resolved_config_path,
config
.and_then(|cfg| cfg.jacs_data_directory().as_deref())
.unwrap_or("./jacs_data"),
);
let public_key_filename = config
.and_then(|cfg| cfg.jacs_agent_public_key_filename().as_deref())
.unwrap_or(DEFAULT_PUBLIC_KEY_FILENAME);
let private_key_filename = config
.and_then(|cfg| cfg.jacs_agent_private_key_filename().as_deref())
.unwrap_or(DEFAULT_PRIVATE_KEY_FILENAME);
Ok(AgentInfo {
agent_id: agent_value["jacsId"].as_str().unwrap_or("").to_string(),
name: agent_value["name"].as_str().unwrap_or("").to_string(),
public_key_path: key_directory
.join(public_key_filename)
.to_string_lossy()
.into_owned(),
config_path: resolved_config_path.to_string_lossy().into_owned(),
version: agent_value["jacsVersion"]
.as_str()
.unwrap_or("")
.to_string(),
algorithm: config
.and_then(|cfg| cfg.jacs_agent_key_algorithm().as_deref())
.unwrap_or("")
.to_string(),
private_key_path: key_directory
.join(private_key_filename)
.to_string_lossy()
.into_owned(),
data_directory: data_directory.to_string_lossy().into_owned(),
key_directory: key_directory.to_string_lossy().into_owned(),
domain: agent_value
.get("jacsAgentDomain")
.and_then(|v| v.as_str())
.or_else(|| agent_value.get("domain").and_then(|v| v.as_str()))
.or_else(|| config.and_then(|cfg| cfg.jacs_agent_domain().as_deref()))
.unwrap_or("")
.to_string(),
dns_record: String::new(),
})
}
pub(crate) 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
}
pub struct SimpleAgent {
pub(crate) agent: Mutex<Agent>,
pub(crate) config_path: Option<String>,
pub(crate) strict: bool,
}
impl SimpleAgent {
pub fn is_strict(&self) -> bool {
self.strict
}
pub fn key_id(&self) -> Result<String, JacsError> {
let agent = self.agent.lock().map_err(|e| JacsError::Internal {
message: format!("Failed to acquire agent lock: {}", e),
})?;
Ok(agent.get_id().unwrap_or_default())
}
#[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 mut builder = CreateAgentParams::builder().name(name);
if let Some(desc) = purpose {
builder = builder.description(desc);
}
if let Some(algo) = key_algorithm {
builder = builder.algorithm(algo);
}
Self::create_with_params(builder.build())
}
#[must_use = "agent creation result must be checked for errors"]
pub fn create_with_params(params: CreateAgentParams) -> Result<(Self, AgentInfo), JacsError> {
use crate::keystore::KeyPaths;
use crate::storage::jenv;
let password = if !params.password.is_empty() {
params.password.clone()
} else {
match crate::crypt::aes_encrypt::resolve_private_key_password(None, None) {
Ok(pw) if !pw.is_empty() => pw,
Ok(_) => {
return Err(JacsError::ConfigError(
"Password is required for agent creation. \
Pass it in CreateAgentParams.password, or set JACS_PRIVATE_KEY_PASSWORD, \
JACS_PASSWORD_FILE, or configure the OS keychain."
.to_string(),
));
}
Err(e) => {
return Err(JacsError::ConfigError(format!(
"Password is required for agent creation. \
Pass it in CreateAgentParams.password, or set JACS_PRIVATE_KEY_PASSWORD, \
JACS_PASSWORD_FILE, or configure the OS keychain. \
Resolution failed: {}",
e
)));
}
}
};
let algorithm = if params.algorithm.is_empty() {
"pq2025".to_string()
} else {
match params.algorithm.as_str() {
"ed25519" => "ring-Ed25519".to_string(),
"rsa-pss" => "RSA-PSS".to_string(),
other => other.to_string(),
}
};
info!(
"Creating new agent '{}' with algorithm '{}' (programmatic)",
params.name, algorithm
);
let keys_dir = Path::new(¶ms.key_directory);
let data_dir = Path::new(¶ms.data_directory);
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.join("agent")).map_err(|e| {
JacsError::DirectoryCreateFailed {
path: data_dir.join("agent").to_string_lossy().to_string(),
reason: e.to_string(),
}
})?;
fs::create_dir_all(data_dir.join("public_keys")).map_err(|e| {
JacsError::DirectoryCreateFailed {
path: data_dir.join("public_keys").to_string_lossy().to_string(),
reason: e.to_string(),
}
})?;
write_key_directory_ignore_files(keys_dir);
let key_paths = KeyPaths {
key_directory: params.key_directory.clone(),
private_key_filename: DEFAULT_PRIVATE_KEY_FILENAME.to_string(),
public_key_filename: DEFAULT_PUBLIC_KEY_FILENAME.to_string(),
};
const JENV_CONFIG_KEYS: [&str; 6] = [
"JACS_DATA_DIRECTORY",
"JACS_KEY_DIRECTORY",
"JACS_AGENT_KEY_ALGORITHM",
"JACS_DEFAULT_STORAGE",
"JACS_AGENT_PRIVATE_KEY_FILENAME",
"JACS_AGENT_PUBLIC_KEY_FILENAME",
];
let saved_jenv: Vec<(&str, bool, Option<String>)> = JENV_CONFIG_KEYS
.iter()
.map(|&key| {
let had_override = jenv::has_jenv_override(key);
let value = if had_override {
jenv::get_env_var(key, false).ok().flatten()
} else {
None
};
(key, had_override, value)
})
.collect();
struct JenvRestoreGuard<'a>(Vec<(&'a str, bool, Option<String>)>);
impl<'a> Drop for JenvRestoreGuard<'a> {
fn drop(&mut self) {
for (key, had_override, prev) in &self.0 {
if *had_override {
if let Some(val) = prev {
let _ = jenv::set_env_var(key, val);
} else {
let _ = jenv::clear_env_var(key);
}
} else {
let _ = jenv::clear_env_var(key);
}
}
}
}
let _jenv_guard = JenvRestoreGuard(saved_jenv);
jenv::set_env_var("JACS_DATA_DIRECTORY", ¶ms.data_directory)?;
jenv::set_env_var("JACS_KEY_DIRECTORY", ¶ms.key_directory)?;
jenv::set_env_var("JACS_AGENT_KEY_ALGORITHM", &algorithm)?;
jenv::set_env_var("JACS_DEFAULT_STORAGE", ¶ms.default_storage)?;
jenv::set_env_var(
"JACS_AGENT_PRIVATE_KEY_FILENAME",
DEFAULT_PRIVATE_KEY_FILENAME,
)?;
jenv::set_env_var(
"JACS_AGENT_PUBLIC_KEY_FILENAME",
DEFAULT_PUBLIC_KEY_FILENAME,
)?;
let description = if params.description.is_empty() {
"JACS agent".to_string()
} else {
params.description.clone()
};
let agent_json = build_agent_document(¶ms.agent_type, ¶ms.name, &description)?;
let mut agent = crate::get_empty_agent();
agent.set_key_paths(key_paths.clone());
agent.set_password(Some(password.clone()));
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);
let config_path = Path::new(¶ms.config_path);
let config_str = if config_path.exists() {
let existing_str =
fs::read_to_string(config_path).map_err(|e| JacsError::Internal {
message: format!(
"Failed to read existing config '{}': {}",
params.config_path, e
),
})?;
let mut existing: serde_json::Value =
serde_json::from_str(&existing_str).map_err(|e| JacsError::Internal {
message: format!("Failed to parse existing config: {}", e),
})?;
let check = |field: &str, existing_val: Option<&str>, param_val: &str| {
if let Some(ev) = existing_val {
if ev != param_val {
warn!(
"Config '{}' differs: existing='{}', param='{}'. Keeping existing value.",
field, ev, param_val
);
}
}
};
check(
"jacs_data_directory",
existing.get("jacs_data_directory").and_then(|v| v.as_str()),
¶ms.data_directory,
);
check(
"jacs_key_directory",
existing.get("jacs_key_directory").and_then(|v| v.as_str()),
¶ms.key_directory,
);
check(
"jacs_agent_key_algorithm",
existing
.get("jacs_agent_key_algorithm")
.and_then(|v| v.as_str()),
&algorithm,
);
check(
"jacs_default_storage",
existing
.get("jacs_default_storage")
.and_then(|v| v.as_str()),
¶ms.default_storage,
);
if let Some(obj) = existing.as_object_mut() {
obj.insert("jacs_agent_id_and_version".to_string(), json!(lookup_id));
}
let updated_str =
serde_json::to_string_pretty(&existing).map_err(|e| JacsError::Internal {
message: format!("Failed to serialize updated config: {}", e),
})?;
fs::write(config_path, &updated_str).map_err(|e| JacsError::Internal {
message: format!("Failed to write config to '{}': {}", params.config_path, e),
})?;
info!(
"Updated existing config '{}' with new agent ID {}",
params.config_path, lookup_id
);
updated_str
} else {
let mut config_map = serde_json::Map::new();
config_map.insert(
"$schema".to_string(),
json!("https://hai.ai/schemas/jacs.config.schema.json"),
);
config_map.insert("jacs_agent_id_and_version".to_string(), json!(lookup_id));
config_map.insert("jacs_agent_key_algorithm".to_string(), json!(algorithm));
config_map.insert(
"jacs_data_directory".to_string(),
json!(params.data_directory),
);
config_map.insert(
"jacs_key_directory".to_string(),
json!(params.key_directory),
);
config_map.insert(
"jacs_default_storage".to_string(),
json!(params.default_storage),
);
config_map.insert(
"jacs_agent_private_key_filename".to_string(),
json!(DEFAULT_PRIVATE_KEY_FILENAME),
);
config_map.insert(
"jacs_agent_public_key_filename".to_string(),
json!(DEFAULT_PUBLIC_KEY_FILENAME),
);
let config_json = Value::Object(config_map);
let new_str =
serde_json::to_string_pretty(&config_json).map_err(|e| JacsError::Internal {
message: format!("Failed to serialize config: {}", e),
})?;
if let Some(parent) = config_path.parent() {
if !parent.as_os_str().is_empty() {
fs::create_dir_all(parent).map_err(|e| JacsError::DirectoryCreateFailed {
path: parent.to_string_lossy().to_string(),
reason: e.to_string(),
})?;
}
}
fs::write(config_path, &new_str).map_err(|e| JacsError::Internal {
message: format!("Failed to write config to '{}': {}", params.config_path, e),
})?;
info!(
"Created new config '{}' for agent {}",
params.config_path, lookup_id
);
new_str
};
let validated_config_value =
crate::config::validate_config(&config_str).map_err(|e| JacsError::Internal {
message: format!("Failed to validate config: {}", e),
})?;
agent.config = Some(serde_json::from_value(validated_config_value).map_err(|e| {
JacsError::Internal {
message: format!("Failed to parse config: {}", e),
}
})?);
agent.save().map_err(|e| JacsError::Internal {
message: format!("Failed to save agent: {}", e),
})?;
if let Some(custom_storage) = params.storage.clone() {
agent.set_storage(custom_storage);
}
let mut dns_record = String::new();
if !params.domain.is_empty() {
if let Ok(pk) = agent.get_public_key() {
let digest = crate::dns::bootstrap::pubkey_digest_b64(&pk);
let rr = crate::dns::bootstrap::build_dns_record(
¶ms.domain,
3600,
&agent_id,
&digest,
crate::dns::bootstrap::DigestEncoding::Base64,
);
dns_record = crate::dns::bootstrap::emit_plain_bind(&rr);
}
}
let private_key_path = format!("{}/{}", params.key_directory, DEFAULT_PRIVATE_KEY_FILENAME);
let public_key_path = format!("{}/{}", params.key_directory, DEFAULT_PUBLIC_KEY_FILENAME);
info!(
"Agent '{}' created successfully with ID {} (programmatic)",
params.name, agent_id
);
let info = AgentInfo {
agent_id,
name: params.name.clone(),
public_key_path,
config_path: params.config_path.clone(),
version,
algorithm: algorithm.clone(),
private_key_path,
data_directory: params.data_directory.clone(),
key_directory: params.key_directory.clone(),
domain: params.domain.clone(),
dns_record,
};
Ok((
Self {
agent: Mutex::new(agent),
config_path: Some(params.config_path),
strict: resolve_strict(None),
},
info,
))
}
#[must_use = "agent loading result must be checked for errors"]
pub fn load(config_path: Option<&str>, strict: Option<bool>) -> Result<Self, JacsError> {
let path = config_path.unwrap_or("./jacs.config.json");
let resolved_path = if Path::new(path).is_absolute() {
normalize_path(Path::new(path))
} else {
normalize_path(&std::env::current_dir()?.join(path))
};
debug!("Loading agent from config: {}", path);
if !resolved_path.exists() {
return Err(JacsError::ConfigNotFound {
path: path.to_string(),
});
}
let mut agent = crate::get_empty_agent();
agent
.load_by_config(resolved_path.to_string_lossy().into_owned())
.map_err(|e| JacsError::ConfigInvalid {
field: "config".to_string(),
reason: e.to_string(),
})?;
info!("Agent loaded successfully from {}", resolved_path.display());
Ok(Self {
agent: Mutex::new(agent),
config_path: Some(resolved_path.to_string_lossy().into_owned()),
strict: resolve_strict(strict),
})
}
pub fn loaded_info(&self) -> Result<AgentInfo, JacsError> {
let config_path = self
.config_path
.as_deref()
.ok_or(JacsError::AgentNotLoaded)?
.to_string();
let agent = self.agent.lock().map_err(|e| JacsError::Internal {
message: format!("Failed to acquire agent lock: {}", e),
})?;
build_loaded_agent_info(&agent, &config_path)
}
#[must_use = "ephemeral agent result must be checked for errors"]
pub fn ephemeral(algorithm: Option<&str>) -> Result<(Self, AgentInfo), JacsError> {
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| JacsError::Internal {
message: format!("Failed to create ephemeral agent: {}", e),
})?;
let agent_json = build_agent_document("ai", "ephemeral", "Ephemeral JACS agent")?;
let instance = agent
.create_agent_and_load(&agent_json.to_string(), true, Some(algo))
.map_err(|e| JacsError::Internal {
message: 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 info = AgentInfo {
agent_id,
name: "ephemeral".to_string(),
public_key_path: String::new(),
config_path: String::new(),
version,
algorithm: algo.to_string(),
private_key_path: String::new(),
data_directory: String::new(),
key_directory: String::new(),
domain: String::new(),
dns_record: String::new(),
};
Ok((
Self {
agent: Mutex::new(agent),
config_path: None,
strict: resolve_strict(None),
},
info,
))
}
#[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();
if self.strict && !valid {
return Err(JacsError::SignatureVerificationFailed {
reason: errors.join("; "),
});
}
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 = "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
),
})?;
info!("Message signed: document_id={}", jacs_doc.id);
SignedDocument::from_jacs_document(jacs_doc, "document")
}
pub fn sign_raw_bytes(&self, data: &[u8]) -> Result<Vec<u8>, JacsError> {
use crate::crypt::KeyManager;
use base64::Engine;
let data_str = std::str::from_utf8(data).map_err(|e| JacsError::Internal {
message: format!("Data is not valid UTF-8: {}", e),
})?;
let mut agent = self.agent.lock().map_err(|e| JacsError::Internal {
message: format!("Failed to acquire agent lock: {}", e),
})?;
let sig_b64 = agent
.sign_string(data_str)
.map_err(|e| JacsError::SigningFailed {
reason: format!("Raw byte signing failed: {}", e),
})?;
let sig_bytes = base64::engine::general_purpose::STANDARD
.decode(&sig_b64)
.map_err(|e| JacsError::Internal {
message: format!("Failed to decode signature base64: {}", e),
})?;
Ok(sig_bytes)
}
pub fn get_agent_id(&self) -> Result<String, JacsError> {
let agent_json = self.export_agent()?;
let doc: serde_json::Value =
serde_json::from_str(&agent_json).map_err(|e| JacsError::Internal {
message: format!("Failed to parse agent JSON: {}", e),
})?;
let agent_id = doc
.pointer("/jacsId")
.or_else(|| doc.pointer("/jacsAgentID"))
.or_else(|| doc.pointer("/id"))
.and_then(|v| v.as_str())
.ok_or_else(|| JacsError::Internal {
message: "Agent ID not found in agent document".to_string(),
})?;
Ok(agent_id.to_string())
}
pub fn set_storage(&self, storage: crate::storage::MultiStorage) -> Result<(), JacsError> {
let mut agent = self.agent.lock().map_err(|e| JacsError::Internal {
message: format!("Failed to acquire agent lock: {}", e),
})?;
agent.set_storage(storage);
Ok(())
}
#[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
),
})?;
SignedDocument::from_jacs_document(jacs_doc, "document")
}
#[must_use = "verification result must be checked"]
pub fn verify(&self, signed_document: &str) -> Result<VerificationResult, JacsError> {
debug!("verify() called");
Self::validate_json_input(signed_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(signed_document)
.map_err(|e| JacsError::DocumentMalformed {
field: "document".to_string(),
reason: e.to_string(),
})?;
let document_key = jacs_doc.getkey();
let mut errors = Vec::new();
if let Err(e) = agent.verify_document_signature(&document_key, None, None, None, None) {
errors.push(e.to_string());
}
if let Err(e) = agent.verify_hash(&jacs_doc.value) {
errors.push(format!("Hash verification failed: {}", e));
}
self.build_verification_result(&jacs_doc.value, errors, "Document verified")
}
#[must_use = "verification result must be checked"]
pub fn verify_with_key(
&self,
signed_document: &str,
public_key: Vec<u8>,
) -> Result<VerificationResult, JacsError> {
debug!("verify_with_key() called");
Self::validate_json_input(signed_document)?;
let mut agent = self.agent.lock().map_err(|e| JacsError::Internal {
message: format!("Failed to acquire agent lock: {}", e),
})?;
let mut errors = Vec::new();
let jacs_doc = match agent.load_document(signed_document) {
Ok(doc) => doc,
Err(e) if !self.strict => {
let value: Value = serde_json::from_str(signed_document).map_err(|parse_err| {
JacsError::DocumentMalformed {
field: "json".to_string(),
reason: parse_err.to_string(),
}
})?;
errors.push(format!("Document load failed: {}", e));
return self.build_verification_result(&value, errors, "Document load failed");
}
Err(e) => {
return Err(JacsError::DocumentMalformed {
field: "document".to_string(),
reason: e.to_string(),
});
}
};
let document_key = jacs_doc.getkey();
if let Err(e) =
agent.verify_document_signature(&document_key, None, None, Some(public_key), None)
{
errors.push(e.to_string());
}
if let Err(e) = agent.verify_hash(&jacs_doc.value) {
errors.push(format!("Hash verification failed: {}", e));
}
self.build_verification_result(&jacs_doc.value, errors, "Document verified with key")
}
fn validate_json_input(signed_document: &str) -> Result<(), JacsError> {
let trimmed = signed_document.trim();
if !trimmed.is_empty() && !trimmed.starts_with('{') && !trimmed.starts_with('[') {
return Err(JacsError::DocumentMalformed {
field: "json".to_string(),
reason: format!(
"Input does not appear to be a JSON document. \
If you have a document ID (e.g., 'uuid:version'), use verify_by_id() instead. \
Received: '{}'",
if trimmed.len() > 60 {
&trimmed[..60]
} else {
trimmed
}
),
});
}
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(),
})?;
Ok(())
}
fn build_verification_result(
&self,
doc_value: &Value,
errors: Vec<String>,
log_label: &str,
) -> Result<VerificationResult, JacsError> {
let valid = errors.is_empty();
if self.strict && !valid {
return Err(JacsError::SignatureVerificationFailed {
reason: errors.join("; "),
});
}
let signer_id = doc_value.get_path_str_or(&["jacsSignature", "agentID"], "");
let timestamp = doc_value.get_path_str_or(&["jacsSignature", "date"], "");
info!("{}: valid={}, signer={}", log_label, valid, signer_id);
let data = if let Some(content) = doc_value.get("content") {
content.clone()
} else {
doc_value.clone()
};
let attachments = extract_attachments(doc_value);
Ok(VerificationResult {
valid,
data,
signer_id,
signer_name: None,
timestamp,
attachments,
errors,
})
}
#[must_use = "verification result must be checked"]
pub fn verify_by_id(&self, document_id: &str) -> Result<VerificationResult, JacsError> {
debug!("verify_by_id() called with id: {}", document_id);
let parts: Vec<&str> = document_id.splitn(2, ':').collect();
if parts.len() != 2 {
return Err(JacsError::DocumentMalformed {
field: "document_id".to_string(),
reason: format!(
"Expected format 'uuid:version', got '{}'. \
Use verify() with the full JSON document string instead.",
document_id
),
});
}
let doc_str = {
let agent = self.agent.lock().map_err(|e| JacsError::Internal {
message: format!("Failed to acquire agent lock: {}", e),
})?;
let jacs_doc = agent
.get_document(document_id)
.map_err(|e| JacsError::Internal {
message: format!(
"Failed to load document '{}' from agent storage: {}",
document_id, e
),
})?;
serde_json::to_string(&jacs_doc.value).map_err(|e| JacsError::Internal {
message: format!("Failed to serialize document '{}': {}", document_id, e),
})?
};
self.verify(&doc_str)
}
#[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(&self) -> Result<Vec<u8>, JacsError> {
let agent = self.agent.lock().map_err(|e| JacsError::Internal {
message: format!("Failed to acquire agent lock: {}", e),
})?;
use crate::agent::boilerplate::BoilerPlate;
agent.get_public_key().map_err(|e| JacsError::Internal {
message: format!("Failed to get public key: {}", e),
})
}
#[must_use = "public key data must be used"]
pub fn get_public_key_pem(&self) -> Result<String, JacsError> {
let agent = self.agent.lock().map_err(|e| JacsError::Internal {
message: format!("Failed to acquire agent lock: {}", e),
})?;
let public_key = agent.get_public_key().map_err(|e| JacsError::Internal {
message: format!("Failed to get public key: {}", e),
})?;
Ok(crate::crypt::normalize_public_key_pem(&public_key))
}
pub fn diagnostics(&self) -> serde_json::Value {
let mut info = super::diagnostics();
if let Ok(agent) = self.agent.lock() {
if agent.ready() {
info["agent_loaded"] = serde_json::json!(true);
if let Some(value) = agent.get_value() {
info["agent_id"] =
serde_json::json!(value.get("jacsId").and_then(|v| v.as_str()));
info["agent_version"] =
serde_json::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"] = serde_json::json!(dir);
}
if let Some(dir) = config.jacs_key_directory().as_ref() {
info["key_directory"] = serde_json::json!(dir);
}
if let Some(storage) = config.jacs_default_storage().as_ref() {
info["default_storage"] = serde_json::json!(storage);
}
if let Some(algo) = config.jacs_agent_key_algorithm().as_ref() {
info["key_algorithm"] = serde_json::json!(algo);
}
}
}
info
}
pub fn config_path(&self) -> Option<&str> {
self.config_path.as_deref()
}
}