use crate::agent::Agent;
use crate::agent::JACS_VERSION_DATE_FIELDNAME;
use crate::agent::JACS_VERSION_FIELDNAME;
use crate::agent::boilerplate::BoilerPlate;
use crate::agent::document::{DocumentTraits, JACSDocument};
use crate::agent::loaders::FileLoader;
use crate::agent::{
AGENT_AGREEMENT_FIELDNAME, DOCUMENT_AGENT_SIGNATURE_FIELDNAME,
DOCUMENT_AGREEMENT_HASH_FIELDNAME, JACS_PREVIOUS_VERSION_FIELDNAME, SHA256_FIELDNAME,
};
use crate::crypt::hash::hash_public_key;
use crate::crypt::hash::hash_string;
use crate::error::JacsError;
use crate::schema::utils::ValueExt;
use crate::validation::normalize_agent_id;
use chrono::Utc;
use serde_json::Value;
use serde_json::json;
use std::collections::HashSet;
use tracing::{debug, info, warn};
#[derive(Debug, Clone, Default)]
pub struct AgreementOptions {
pub timeout: Option<String>,
pub quorum: Option<u32>,
pub required_algorithms: Option<Vec<String>>,
pub minimum_strength: Option<String>,
}
pub fn algorithm_strength(algo: &str) -> &'static str {
match algo {
"pq2025" => "post-quantum",
_ => "classical",
}
}
fn meets_strength_requirement(algo: &str, minimum_strength: &str) -> bool {
match minimum_strength {
"post-quantum" => algorithm_strength(algo) == "post-quantum",
_ => true,
}
}
pub trait Agreement {
fn create_agreement(
&mut self,
document_key: &str,
agentids: &[String],
question: Option<&str>,
context: Option<&str>,
agreement_fieldname: Option<String>,
) -> Result<JACSDocument, JacsError>;
fn create_agreement_with_options(
&mut self,
document_key: &str,
agentids: &[String],
question: Option<&str>,
context: Option<&str>,
agreement_fieldname: Option<String>,
options: &AgreementOptions,
) -> Result<JACSDocument, JacsError>;
fn add_agents_to_agreement(
&mut self,
document_key: &str,
agentids: &[String],
agreement_fieldname: Option<String>,
) -> Result<JACSDocument, JacsError>;
fn remove_agents_from_agreement(
&mut self,
document_key: &str,
agentids: &[String],
agreement_fieldname: Option<String>,
) -> Result<JACSDocument, JacsError>;
fn sign_agreement(
&mut self,
document_key: &str,
agreement_fieldname: Option<String>,
) -> Result<JACSDocument, JacsError>;
fn check_agreement(
&self,
document_key: &str,
agreement_fieldname: Option<String>,
) -> Result<String, JacsError>;
fn agreement_hash(&self, value: Value, agreement_fieldname: &str) -> Result<String, JacsError>;
fn trim_fields_for_hashing_and_signing(
&self,
value: Value,
agreement_fieldname: &str,
) -> Result<(String, Vec<String>), JacsError>;
fn agreement_get_question_and_context(
&self,
document_key: &str,
agreement_fieldname: Option<String>,
) -> Result<(String, String), JacsError>;
}
impl Agreement for Agent {
fn agreement_hash(&self, value: Value, agreement_fieldname: &str) -> Result<String, JacsError> {
let (values_as_string, _fields) =
self.trim_fields_for_hashing_and_signing(value, agreement_fieldname)?;
Ok(hash_string(&values_as_string))
}
fn trim_fields_for_hashing_and_signing(
&self,
value: Value,
agreement_fieldname: &str,
) -> Result<(String, Vec<String>), JacsError> {
let mut new_obj: Value = value.clone();
new_obj.as_object_mut().map(|obj| {
obj.remove(DOCUMENT_AGREEMENT_HASH_FIELDNAME);
obj.remove(JACS_PREVIOUS_VERSION_FIELDNAME);
obj.remove(JACS_VERSION_FIELDNAME);
obj.remove(JACS_VERSION_DATE_FIELDNAME)
});
if new_obj.get("jacsFiles").is_none()
&& let Some(obj) = new_obj.as_object_mut()
{
obj.insert("jacsFiles".to_string(), json!([]));
}
let (values_as_string, fields) =
Agent::get_values_as_string(&new_obj, None, agreement_fieldname)?;
Ok((values_as_string, fields))
}
fn create_agreement(
&mut self,
document_key: &str,
agentids: &[String],
question: Option<&str>,
context: Option<&str>,
agreement_fieldname: Option<String>,
) -> Result<JACSDocument, JacsError> {
self.create_agreement_with_options(
document_key,
agentids,
question,
context,
agreement_fieldname,
&AgreementOptions::default(),
)
}
fn create_agreement_with_options(
&mut self,
document_key: &str,
agentids: &[String],
question: Option<&str>,
context: Option<&str>,
agreement_fieldname: Option<String>,
options: &AgreementOptions,
) -> Result<JACSDocument, JacsError> {
let agreement_fieldname_key = match agreement_fieldname {
Some(key) => key,
_ => AGENT_AGREEMENT_FIELDNAME.to_string(),
};
if let Some(q) = options.quorum {
if q == 0 {
return Err(JacsError::DocumentError(
"Quorum must be at least 1".to_string(),
));
}
if q as usize > agentids.len() {
return Err(JacsError::DocumentError(format!(
"Quorum ({}) cannot exceed the number of agents ({})",
q,
agentids.len()
)));
}
}
if let Some(ref timeout_str) = options.timeout {
let deadline = chrono::DateTime::parse_from_rfc3339(timeout_str).map_err(|e| {
JacsError::DocumentError(format!(
"Invalid timeout '{}': must be ISO 8601 date-time (e.g., '2025-12-31T23:59:59Z'). Error: {}",
timeout_str, e
))
})?;
if deadline <= Utc::now() {
return Err(JacsError::DocumentError(format!(
"Timeout '{}' is in the past",
timeout_str
)));
}
}
if let Some(ref strength) = options.minimum_strength {
if strength != "classical" && strength != "post-quantum" {
return Err(JacsError::DocumentError(format!(
"Invalid minimumStrength '{}': must be 'classical' or 'post-quantum'",
strength
)));
}
}
let document = self.get_document(document_key)?;
let mut value = document.value;
let context_string = context.unwrap_or_default();
let question_string = question.unwrap_or_default();
let agreement_hash_value =
json!(self.agreement_hash(value.clone(), &agreement_fieldname_key)?);
value[DOCUMENT_AGREEMENT_HASH_FIELDNAME] = agreement_hash_value.clone();
let mut agreement_obj = json!({
"signatures": [],
"agentIDs": agentids,
"question": question_string,
"context": context_string
});
if let Some(ref timeout) = options.timeout {
agreement_obj["timeout"] = json!(timeout);
}
if let Some(quorum) = options.quorum {
agreement_obj["quorum"] = json!(quorum);
}
if let Some(ref algos) = options.required_algorithms {
agreement_obj["requiredAlgorithms"] = json!(algos);
}
if let Some(ref strength) = options.minimum_strength {
agreement_obj["minimumStrength"] = json!(strength);
}
value[agreement_fieldname_key.clone()] = agreement_obj;
let updated_document =
self.update_document(document_key, &serde_json::to_string(&value)?, None, None)?;
let agreement_hash_value_after =
json!(self.agreement_hash(updated_document.value.clone(), &agreement_fieldname_key)?);
if agreement_hash_value != agreement_hash_value_after {
return Err(JacsError::DocumentError(format!(
"Agreement field hashes don't match for document_key {}",
document_key
)));
}
if value[SHA256_FIELDNAME] == updated_document.value[SHA256_FIELDNAME] {
return Err(JacsError::DocumentError(format!(
"document hashes should have changed {}",
document_key
)));
};
let quorum_display = options
.quorum
.map(|q| q.to_string())
.unwrap_or_else(|| "all".to_string());
info!(
event = "agreement_created",
document_id = %document_key,
agent_count = agentids.len(),
quorum = %quorum_display,
has_timeout = options.timeout.is_some(),
"Agreement created"
);
Ok(updated_document)
}
fn remove_agents_from_agreement(
&mut self,
document_key: &str,
agentids: &[String],
agreement_fieldname: Option<String>,
) -> Result<JACSDocument, JacsError> {
let agreement_fieldname_key = match agreement_fieldname {
Some(key) => key,
_ => AGENT_AGREEMENT_FIELDNAME.to_string(),
};
let document = self.get_document(document_key)?;
let mut value = document.value;
let _ = value[DOCUMENT_AGREEMENT_HASH_FIELDNAME].clone();
if let Some(jacs_agreement) = value.get_mut(agreement_fieldname_key) {
if let Some(agents) = jacs_agreement.get_mut("agentIDs") {
if let Some(agents_array) = agents.as_array_mut() {
let agents_vec: Vec<String> = agents_array
.iter()
.map(|v| v.as_str().unwrap().to_string())
.collect();
let merged_agents = subtract_vecs(&agents_vec, agentids);
*agents = json!(merged_agents);
} else {
return Err(
"Agreement modification failed: no agents present in agreement".into(),
);
}
} else {
return Err("Agreement modification failed: agents field not found".into());
}
} else {
return Err("Agreement modification failed: no agreement field present".into());
}
let updated_document =
self.update_document(document_key, &serde_json::to_string(&value)?, None, None)?;
Ok(updated_document)
}
fn add_agents_to_agreement(
&mut self,
document_key: &str,
agentids: &[String],
agreement_fieldname: Option<String>,
) -> Result<JACSDocument, JacsError> {
let agreement_fieldname_key = match agreement_fieldname {
Some(key) => key,
_ => AGENT_AGREEMENT_FIELDNAME.to_string(),
};
let document = self.get_document(document_key)?;
let mut value = document.value;
let _ = value[DOCUMENT_AGREEMENT_HASH_FIELDNAME].clone();
let normalized_agent_ids: Vec<String> = agentids
.iter()
.map(|id| {
if let Some(pos) = id.find(':') {
id[0..pos].to_string()
} else {
id.clone()
}
})
.collect();
if let Some(jacs_agreement) = value.get_mut(agreement_fieldname_key.clone()) {
if let Some(agents) = jacs_agreement.get_mut("agentIDs") {
if let Some(agents_array) = agents.as_array_mut() {
let existing_agents: Vec<String> = agents_array
.iter()
.map(|v| {
let id_str = v.as_str().unwrap().to_string();
if let Some(pos) = id_str.find(':') {
id_str[0..pos].to_string()
} else {
id_str
}
})
.collect();
let merged_agents =
merge_without_duplicates(&existing_agents, &normalized_agent_ids);
*agents = json!(merged_agents);
} else {
*agents = json!(normalized_agent_ids);
}
} else {
jacs_agreement["agentIDs"] = json!(normalized_agent_ids);
}
} else {
value[agreement_fieldname_key] = json!({
"agentIDs": normalized_agent_ids,
"signatures": [],
});
}
let updated_document =
self.update_document(document_key, &serde_json::to_string(&value)?, None, None)?;
Ok(updated_document)
}
fn sign_agreement(
&mut self,
document_key: &str,
agreement_fieldname: Option<String>,
) -> Result<JACSDocument, JacsError> {
let agreement_fieldname_key = match agreement_fieldname {
Some(ref key) => key.to_string(),
_ => AGENT_AGREEMENT_FIELDNAME.to_string(),
};
let document = self.get_document(document_key)?;
let mut value = document.value;
if let Some(jacs_agreement) = value.get(&agreement_fieldname_key) {
if let Some(timeout_str) = jacs_agreement.get("timeout").and_then(|v| v.as_str()) {
if let Ok(deadline) = chrono::DateTime::parse_from_rfc3339(timeout_str) {
if Utc::now() > deadline {
warn!(
event = "agreement_expired",
document_id = %document_key,
deadline = %timeout_str,
"Cannot sign expired agreement"
);
return Err(JacsError::DocumentError(format!(
"Cannot sign: agreement has expired (deadline was {})",
timeout_str
)));
}
}
}
if let Some(algo) = &self.key_algorithm {
if let Some(required) = jacs_agreement
.get("requiredAlgorithms")
.and_then(|v| v.as_array())
{
let required_algos: Vec<String> = required
.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect();
if !required_algos.contains(algo) {
return Err(JacsError::DocumentError(format!(
"Cannot sign: agent's algorithm '{}' is not in requiredAlgorithms {:?}",
algo, required_algos
)));
}
}
if let Some(strength) = jacs_agreement
.get("minimumStrength")
.and_then(|v| v.as_str())
{
if !meets_strength_requirement(algo, strength) {
return Err(JacsError::DocumentError(format!(
"Cannot sign: agent's algorithm '{}' does not meet minimumStrength '{}'",
algo, strength
)));
}
}
}
}
let binding = value[DOCUMENT_AGREEMENT_HASH_FIELDNAME].clone();
let original_agreement_hash_value = binding.as_str();
let _calculated_agreement_hash_value =
self.agreement_hash(value.clone(), &agreement_fieldname_key)?;
let signing_agent_id = self.get_id().expect("agent id");
let (_values_as_string, fields) =
self.trim_fields_for_hashing_and_signing(value.clone(), &agreement_fieldname_key)?;
let agents_signature: Value = self.signing_procedure(
&value.clone(),
Some(&fields),
&agreement_fieldname_key.to_string(),
)?;
let normalized_agent_id = normalize_agent_id(&signing_agent_id).to_string();
let mut agent_already_in_agreement = false;
if let Some(jacs_agreement) = value.get(agreement_fieldname_key.clone())
&& let Some(agents) = jacs_agreement.get("agentIDs")
&& let Some(agents_array) = agents.as_array()
{
for agent in agents_array {
let agent_str = agent.as_str().unwrap_or("");
if normalize_agent_id(agent_str) == normalized_agent_id {
agent_already_in_agreement = true;
break;
}
}
}
if !agent_already_in_agreement {
if let Some(jacs_agreement) = value.get_mut(&agreement_fieldname_key) {
if let Some(agent_ids) = jacs_agreement.get_mut("agentIDs") {
if let Some(agent_ids_array) = agent_ids.as_array_mut() {
agent_ids_array.push(json!(normalized_agent_id.clone()));
} else {
*agent_ids = json!([normalized_agent_id.clone()]);
}
} else {
jacs_agreement["agentIDs"] = json!([normalized_agent_id.clone()]);
}
} else {
value[agreement_fieldname_key.clone()] = json!({
"agentIDs": [normalized_agent_id.clone()],
"signatures": []
});
}
}
debug!(
"agents_signature {}",
serde_json::to_string_pretty(&agents_signature).expect("agents_signature print")
);
if let Some(jacs_agreement) = value.get_mut(&agreement_fieldname_key) {
if let Some(signatures) = jacs_agreement.get_mut("signatures") {
if let Some(signatures_array) = signatures.as_array_mut() {
signatures_array.push(agents_signature);
} else {
*signatures = json!([agents_signature]);
}
} else {
jacs_agreement["signatures"] = json!([agents_signature]);
}
} else {
value[agreement_fieldname_key.clone()] = json!({
"agentIDs": [normalized_agent_id],
"signatures": [agents_signature]
});
}
let pre_update_hash = value[SHA256_FIELDNAME].clone();
let new_version = uuid::Uuid::new_v4().to_string();
let last_version = value[JACS_VERSION_FIELDNAME].clone();
value[JACS_PREVIOUS_VERSION_FIELDNAME] = last_version;
value[JACS_VERSION_FIELDNAME] = json!(new_version);
value[JACS_VERSION_DATE_FIELDNAME] = json!(crate::time_utils::now_rfc3339());
value[DOCUMENT_AGENT_SIGNATURE_FIELDNAME] =
self.signing_procedure(&value, None, DOCUMENT_AGENT_SIGNATURE_FIELDNAME)?;
let document_hash = self.hash_doc(&value)?;
value[SHA256_FIELDNAME] = json!(document_hash);
let updated_document = self.store_jacs_document(&value)?;
let agreement_hash_value_after =
self.agreement_hash(updated_document.value.clone(), &agreement_fieldname_key)?;
if original_agreement_hash_value != Some(&agreement_hash_value_after) {
return Err(JacsError::DocumentError(format!(
"aborting signature on agreement. field hashes don't match for document_key {} \n {} {}",
document_key,
original_agreement_hash_value.expect("original_agreement_hash_value"),
agreement_hash_value_after
)));
}
if pre_update_hash == updated_document.value[SHA256_FIELDNAME] {
return Err(JacsError::DocumentError(format!(
"document hashes should have changed {}",
document_key
)));
};
let sig_count = updated_document
.value
.get(&agreement_fieldname_key)
.and_then(|a| a.get("signatures"))
.and_then(|s| s.as_array())
.map(|a| a.len())
.unwrap_or(0);
let total_agents = updated_document
.value
.get(&agreement_fieldname_key)
.and_then(|a| a.get("agentIDs"))
.and_then(|a| a.as_array())
.map(|a| a.len())
.unwrap_or(0);
let quorum = updated_document
.value
.get(&agreement_fieldname_key)
.and_then(|a| a.get("quorum"))
.and_then(|v| v.as_u64())
.map(|v| v as usize);
let required = quorum.unwrap_or(total_agents);
info!(
event = "signature_added",
document_id = %document_key,
signer_id = %normalized_agent_id,
current = sig_count,
total = total_agents,
required = required,
"Agreement signature added"
);
if sig_count >= required {
info!(
event = "quorum_reached",
document_id = %document_key,
signatures = sig_count,
required = required,
total = total_agents,
"Agreement quorum reached"
);
}
Ok(updated_document)
}
fn agreement_get_question_and_context(
&self,
document_key: &str,
agreement_fieldname: Option<String>,
) -> Result<(String, String), JacsError> {
let agreement_fieldname_key = match agreement_fieldname {
Some(key) => key,
_ => AGENT_AGREEMENT_FIELDNAME.to_string(),
};
let document = self.get_document(document_key)?;
let error_message = format!("{} missing", DOCUMENT_AGREEMENT_HASH_FIELDNAME);
let original_agreement_hash_value = document.value[DOCUMENT_AGREEMENT_HASH_FIELDNAME]
.as_str()
.expect(&error_message);
let calculated_agreement_hash_value =
self.agreement_hash(document.value.clone(), &agreement_fieldname_key)?;
if original_agreement_hash_value != calculated_agreement_hash_value {
return Err("Agreement verification failed: agreement hashes do not match".into());
}
if let Some(jacs_agreement) = document.value.get(agreement_fieldname_key) {
let question = jacs_agreement
.get_str("question")
.expect("agreement_get_question_and_context question field");
let context = jacs_agreement
.get_str("context")
.expect("agreement_get_question_and_context question field");
return Ok((question.to_string(), context.to_string()));
}
Err("Agreement verification failed: document has no agreement".into())
}
fn check_agreement(
&self,
document_key: &str,
agreement_fieldname: Option<String>,
) -> Result<String, JacsError> {
let agreement_fieldname_key: String = match agreement_fieldname {
Some(ref key) => key.to_string(),
_ => AGENT_AGREEMENT_FIELDNAME.to_string(),
};
let document = self.get_document(document_key)?;
let local_doc_value = document.value.clone();
let error_message = format!("{} missing", DOCUMENT_AGREEMENT_HASH_FIELDNAME);
let original_agreement_hash_value = document.value[DOCUMENT_AGREEMENT_HASH_FIELDNAME]
.as_str()
.expect(&error_message);
let calculated_agreement_hash_value =
self.agreement_hash(document.value.clone(), &agreement_fieldname_key)?;
if original_agreement_hash_value != calculated_agreement_hash_value {
return Err("Agreement verification failed: agreement hashes do not match".into());
}
let jacs_agreement = document
.value
.get(agreement_fieldname_key.clone())
.ok_or("Agreement verification failed: document has no agreement")?;
if let Some(timeout_str) = jacs_agreement.get("timeout").and_then(|v| v.as_str()) {
if let Ok(deadline) = chrono::DateTime::parse_from_rfc3339(timeout_str) {
if Utc::now() > deadline {
warn!(
event = "agreement_expired",
document_id = %document_key,
deadline = %timeout_str,
"Agreement has expired"
);
return Err(JacsError::DocumentError(format!(
"Agreement has expired: deadline was {}",
timeout_str
)));
}
}
}
let quorum: Option<u32> = jacs_agreement
.get("quorum")
.and_then(|v| v.as_u64())
.map(|v| v as u32);
let required_algorithms: Option<Vec<String>> = jacs_agreement
.get("requiredAlgorithms")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect()
});
let minimum_strength: Option<&str> = jacs_agreement
.get("minimumStrength")
.and_then(|v| v.as_str());
let unsigned = document.agreement_unsigned_agents(agreement_fieldname.clone())?;
let all_agents = document.agreement_requested_agents(agreement_fieldname.clone())?;
let signed_count = all_agents.len() - unsigned.len();
if let Some(q) = quorum {
if (signed_count as u32) < q {
return Err(JacsError::DocumentError(format!(
"Quorum not met: need {} signatures, have {} (unsigned: {:?})",
q, signed_count, unsigned
)));
}
} else {
if !unsigned.is_empty() {
return Err(JacsError::DocumentError(format!(
"not all agents have signed: {:?} {:?}",
unsigned, jacs_agreement
)));
}
}
if let Some(signatures) = jacs_agreement.get("signatures")
&& let Some(signatures_array) = signatures.as_array()
{
for signature in signatures_array {
let agent_id_and_version = format!(
"{}:{}",
signature
.get_str("agentID")
.expect("REASON agreement signature agentID"),
signature
.get_str("agentVersion")
.expect("REASON agreement signature agentVersion")
)
.to_string();
let noted_hash = signature
.get_str("publicKeyHash")
.expect("REASON noted_hash")
.to_string();
let public_key_enc_type = signature
.get_str("signingAlgorithm")
.expect("REASON public_key_enc_type")
.to_string();
if let Some(ref algos) = required_algorithms {
if !algos.contains(&public_key_enc_type) {
return Err(JacsError::DocumentError(format!(
"Signature from {} uses algorithm '{}' which is not in requiredAlgorithms {:?}",
agent_id_and_version, public_key_enc_type, algos
)));
}
}
if let Some(strength) = minimum_strength {
if !meets_strength_requirement(&public_key_enc_type, strength) {
return Err(JacsError::DocumentError(format!(
"Signature from {} uses algorithm '{}' which does not meet minimumStrength '{}'",
agent_id_and_version, public_key_enc_type, strength
)));
}
}
let agents_signature = signature
.get_str("signature")
.expect("REASON public_key_enc_type")
.to_string();
let agents_public_key = self.fs_load_public_key(¬ed_hash)?;
let new_hash = hash_public_key(&agents_public_key);
if new_hash != noted_hash {
return Err(JacsError::CryptoError(format!(
"wrong public key for {} , {}",
agent_id_and_version, noted_hash
)));
}
debug!(
"testing agreement sig agent_id_and_version {} {} {} ",
agent_id_and_version, noted_hash, public_key_enc_type
);
let (_values_as_string, fields) = self.trim_fields_for_hashing_and_signing(
local_doc_value.clone(),
&agreement_fieldname_key,
)?;
let mut signature_context = document.value.clone();
signature_context[agreement_fieldname_key.clone()] = signature.clone();
self.signature_verification_procedure(
&signature_context,
Some(&fields),
&agreement_fieldname_key.to_string(),
agents_public_key,
Some(public_key_enc_type.clone()),
Some(noted_hash.clone()),
Some(agents_signature),
)?;
}
if let Some(q) = quorum {
return Ok(format!(
"Quorum met: {}/{} signatures verified (required: {})",
signed_count,
all_agents.len(),
q
));
}
return Ok("All signatures passed".to_string());
}
Err("Agreement verification failed: document has no agreement".into())
}
}
pub fn merge_without_duplicates(vec1: &[String], vec2: &[String]) -> Vec<String> {
let mut set: HashSet<String> = HashSet::new();
for item in vec1 {
set.insert(item.to_string());
}
for item in vec2 {
set.insert(item.to_string());
}
set.into_iter().collect()
}
pub fn subtract_vecs(vec1: &[String], vec2: &[String]) -> Vec<String> {
debug!("subtract_vecs A {:?} {:?} ", vec1, vec2);
let to_remove: HashSet<&String> = vec2.iter().collect();
let return_vec1 = vec1
.iter()
.filter(|item| !to_remove.contains(item))
.cloned()
.collect();
debug!("subtract_vecs B {:?}- {:?} = {:?}", vec1, vec2, return_vec1);
return_vec1
}