use crate::error::JacsError;
use crate::schema::utils::CONFIG_SCHEMA_STRING;
use crate::schema::utils::ValueExt;
use crate::time_utils;
use jsonschema::{Draft, Retrieve, Validator};
use referencing::Uri;
use tracing::{debug, error, warn};
use regex::Regex;
use serde_json::Value;
use serde_json::json;
use std::sync::Arc;
use url::Url;
use uuid::Uuid;
pub mod action_crud;
pub mod agent_crud;
pub mod agentstate_crud;
pub mod commitment_crud;
pub mod contact_crud;
pub mod conversation_crud;
pub mod message_crud;
pub mod reference_utils;
pub mod service_crud;
pub mod signature;
pub mod task_crud;
pub mod todo_crud;
pub mod tools_crud;
pub mod utils;
use crate::agent::document::DEFAULT_JACS_DOC_LEVEL;
use utils::{DEFAULT_SCHEMA_STRINGS, EmbeddedSchemaResolver};
#[cfg(not(target_arch = "wasm32"))]
pub use utils::should_accept_invalid_certs_for_claim;
use std::error::Error;
use std::fmt;
fn build_validator(schema: &Value, schema_name: &str) -> Result<Validator, JacsError> {
Validator::options()
.with_draft(Draft::Draft7)
.with_retriever(EmbeddedSchemaResolver::new())
.build(schema)
.map_err(|e| JacsError::SchemaError(format!("Failed to compile {}: {}", schema_name, e)))
}
pub fn format_schema_validation_error(
error: &jsonschema::ValidationError,
schema_name: &str,
instance: &Value,
) -> String {
let path = error.instance_path.to_string();
let field_path = if path.is_empty() || path == "/" {
"root".to_string()
} else {
path.trim_start_matches('/').replace('/', ".").to_string()
};
let actual_value: Option<String> = if !path.is_empty() && path != "/" {
let path_parts: Vec<&str> = path.trim_start_matches('/').split('/').collect();
let mut current = instance;
for part in &path_parts {
match current {
Value::Object(obj) => {
if let Some(val) = obj.get(*part) {
current = val;
} else {
break;
}
}
Value::Array(arr) => {
if let Ok(idx) = part.parse::<usize>() {
if let Some(val) = arr.get(idx) {
current = val;
} else {
break;
}
} else {
break;
}
}
_ => break,
}
}
if current != instance {
let type_name = match current {
Value::Null => "null".to_string(),
Value::Bool(_) => "boolean".to_string(),
Value::Number(_) => "number".to_string(),
Value::String(_) => "string".to_string(),
Value::Array(_) => "array".to_string(),
Value::Object(_) => "object".to_string(),
};
let value_str = current.to_string();
let truncated = if value_str.len() > 50 {
format!("{}...", &value_str[..47])
} else {
value_str
};
Some(format!("{} ({})", type_name, truncated))
} else {
None
}
} else {
None
};
let error_str = error.to_string();
let expected = extract_expected_type(&error_str);
let mut msg = format!(
"Schema validation failed for '{}' at field '{}': {}",
schema_name, field_path, error
);
if let Some(exp) = expected {
if let Some(ref actual) = actual_value {
msg.push_str(&format!(" [expected {}, got {}]", exp, actual));
} else {
msg.push_str(&format!(" [expected {}]", exp));
}
} else if let Some(ref actual) = actual_value {
msg.push_str(&format!(" [got {}]", actual));
}
msg
}
fn extract_expected_type(error_msg: &str) -> Option<String> {
if let Some(pos) = error_msg.find("is not of type ") {
let rest = &error_msg[pos + 15..];
if let Some(end) = rest.find([',', ')', ']']) {
return Some(rest[..end].trim_matches('"').to_string());
}
return Some(rest.trim_matches('"').to_string());
}
if let Some(pos) = error_msg.find("is not one of ") {
let rest = &error_msg[pos + 14..];
return Some(format!("one of {}", rest));
}
if error_msg.contains("is a required property") {
return Some("required property".to_string());
}
if error_msg.contains("is missing") {
return Some("required field".to_string());
}
if error_msg.contains("does not match") {
return Some("matching pattern".to_string());
}
None
}
#[derive(Debug)]
pub struct ValidationError(pub String);
impl fmt::Display for ValidationError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "Validation error: {}", self.0)
}
}
impl Error for ValidationError {}
#[derive(Debug)]
#[allow(dead_code)]
pub struct Schema {
pub headerschema: Validator,
headerversion: String,
pub agentschema: Validator,
signatureschema: Validator,
jacsconfigschema: Validator,
agreementschema: Validator,
serviceschema: Validator,
unitschema: Validator,
actionschema: Validator,
toolschema: Validator,
contactschema: Validator,
pub taskschema: Validator,
messageschema: Validator,
evalschema: Validator,
nodeschema: Validator,
programschema: Validator,
embeddingschema: Validator,
pub agentstateschema: Validator,
pub commitmentschema: Validator,
pub todoschema: Validator,
#[cfg(feature = "attestation")]
pub attestationschema: Validator,
}
static EXCLUDE_FIELDS: [&str; 2] = ["$schema", "$id"];
impl Schema {
fn validate_json_with_schema(
&self,
json: &str,
validator: &Validator,
default_schema_name: &str,
invalid_json_prefix: &str,
) -> Result<Value, JacsError> {
let instance: serde_json::Value = match serde_json::from_str(json) {
Ok(value) => {
debug!("validate json {:?}", value);
value
}
Err(e) => {
let error_message = format!("{}: {}", invalid_json_prefix, e);
warn!("validate error {:?}", error_message);
return Err(JacsError::SchemaError(error_message));
}
};
match validator.validate(&instance) {
Ok(_) => Ok(instance),
Err(error) => {
let schema_name = instance
.get("$schema")
.and_then(|v| v.as_str())
.unwrap_or(default_schema_name);
let error_message = format_schema_validation_error(&error, schema_name, &instance);
error!("{}", error_message);
Err(JacsError::SchemaError(error_message))
}
}
}
pub fn extract_hai_fields(&self, document: &Value, level: &str) -> Result<Value, JacsError> {
let schema_url = document["$schema"]
.as_str()
.unwrap_or("schemas/header/v1/header.schema.json");
let mut processed_fields: Vec<String> = Vec::new();
self._extract_hai_fields(document, schema_url, level, &mut processed_fields)
}
fn _extract_hai_fields(
&self,
document: &Value,
schema_url: &str,
level: &str,
processed_fields: &mut Vec<String>,
) -> Result<Value, JacsError> {
let mut result = json!({});
let schema_resolver = EmbeddedSchemaResolver::new();
let base_url = Url::parse("https://hai.ai")
.map_err(|e| JacsError::SchemaError(format!("Invalid base URL: {}", e)))?;
let url = base_url.join(schema_url).map_err(|e| {
JacsError::SchemaError(format!("Invalid schema URL '{}': {}", schema_url, e))
})?;
let schema_value_result =
schema_resolver
.retrieve(&Uri::try_from(url.as_str().to_string()).map_err(|e| {
JacsError::SchemaError(format!("Invalid URI '{}': {}", url, e))
})?);
let schema_value: Arc<Value> = match schema_value_result {
Err(_) => {
let default_url = Url::parse("https://hai.ai/schemas/header/v1/header.schema.json")
.map_err(|e| JacsError::SchemaError(format!("Invalid default URL: {}", e)))?;
let result = match schema_resolver.retrieve(
&Uri::try_from(default_url.as_str().to_string()).map_err(|e| {
JacsError::SchemaError(format!("Invalid default URI: {}", e))
})?,
) {
Ok(value) => value,
Err(e) => return Err(e.to_string().into()),
};
Arc::new(result)
}
Ok(value) => Arc::new(value),
};
match schema_value.as_ref() {
Value::Object(schema_map) => {
if let Some(all_of) = schema_map.get("allOf") {
if let Value::Array(all_of_array) = all_of {
for item in all_of_array {
if let Some(ref_url) = item.get("$ref")
&& let Some(ref_schema_url) = ref_url.as_str()
{
let child_result = self._extract_hai_fields(
document,
ref_schema_url,
level,
processed_fields,
)?;
if let (Some(result_obj), Some(child_obj)) =
(result.as_object_mut(), child_result.as_object())
{
result_obj.extend(child_obj.clone());
}
}
if let Some(properties) = item.get("properties") {
self.process_properties(
level,
document,
processed_fields,
&mut result,
properties,
)?;
}
}
}
} else if let Some(properties) = schema_map.get("properties") {
self.process_properties(
level,
document,
processed_fields,
&mut result,
properties,
)?;
}
}
_ => return Err("Invalid schema format".into()),
}
if let Some(document_object) = document.as_object() {
for (field_name, field_value) in document_object {
if !processed_fields.contains(field_name)
&& (!EXCLUDE_FIELDS.contains(&field_name.as_str()) || level == "base")
{
result[field_name] = field_value.clone();
}
}
}
Ok(result)
}
fn process_properties(
&self,
level: &str,
document: &Value,
processed_fields: &mut Vec<String>,
result: &mut Value,
properties: &Value,
) -> Result<(), JacsError> {
if let Value::Object(properties_map) = properties {
for (field_name, field_schema) in properties_map {
if field_name == "jacsTaskMessages" || field_name == "attachments" {
debug!(
"\n\n attachments field_name in items {} {:?}\n\n\n\n",
field_name, field_schema
);
}
Self::process_field_value(
level,
result,
field_name,
field_schema.clone(),
document.clone(),
);
processed_fields.push(field_name.clone());
if let Some(ref_url) = field_schema.get("$ref") {
if let Some(ref_schema_url) = ref_url.as_str()
&& let Some(field_value) = document.get(field_name.clone())
{
let mut new_processed_fields = Vec::new();
let child_result = self._extract_hai_fields(
field_value,
ref_schema_url,
level,
&mut new_processed_fields,
)?;
if !child_result.is_null() {
result[field_name] = child_result;
}
}
} else if let Some(items) = field_schema.get("items")
&& let Some(ref_url) = items.get("$ref")
&& let Some(ref_schema_url) = ref_url.as_str()
&& let Some(Value::Array(field_value_array)) = document.get(field_name)
{
let mut items_result = Vec::new();
for item_value in field_value_array {
let mut new_processed_fields = Vec::new();
let child_result = self._extract_hai_fields(
item_value,
ref_schema_url,
level,
&mut new_processed_fields,
)?;
items_result.push(child_result);
}
result[field_name] = Value::Array(items_result);
}
}
return Ok(());
}
Err("Properties map failed: could not extract from schema".into())
}
fn process_field_value(
level: &str,
result: &mut Value,
field_name: &str,
field_schema: Value,
document: Value,
) {
let hai_level = field_schema
.get("hai")
.and_then(|v| v.as_str())
.unwrap_or("");
debug!("properties hai_level {} {}", hai_level, field_name);
match level {
"agent" => {
if hai_level == "agent"
&& let Some(field_value) = document.get(field_name)
{
result[field_name] = field_value.clone();
}
}
"meta" => {
if (hai_level == "agent" || hai_level == "meta")
&& let Some(field_value) = document.get(field_name)
{
result[field_name] = field_value.clone();
}
}
"base" => {
if let Some(field_value) = document.get(field_name) {
result[field_name] = field_value.clone();
}
}
_ => {
if let Some(field_value) = document.get(field_name) {
result[field_name] = field_value.clone();
}
}
}
}
pub fn new(
agentversion: &str,
headerversion: &str,
signatureversion: &str,
) -> Result<Self, JacsError> {
let default_version = "v1";
let header_path = format!("schemas/header/{}/header.schema.json", headerversion);
let agentversion_path = format!("schemas/agent/{}/agent.schema.json", agentversion);
let agreementversion_path = format!(
"schemas/components/agreement/{}/agreement.schema.json",
agentversion
);
let signatureversion_path = format!(
"schemas/components/signature/{}/signature.schema.json",
signatureversion
);
let unit_path = format!(
"schemas/components/unit/{}/unit.schema.json",
default_version
);
let service_path = format!(
"schemas/components/service/{}/service.schema.json",
default_version
);
let action_path = format!(
"schemas/components/action/{}/action.schema.json",
default_version
);
let tool_path = format!(
"schemas/components/tool/{}/tool.schema.json",
default_version
);
let contact_path = format!(
"schemas/components/contact/{}/contact.schema.json",
default_version
);
let task_path = format!("schemas/task/{}/task.schema.json", default_version);
let node_path = format!("schemas/node/{}/node.schema.json", default_version);
let program_path = format!("schemas/program/{}/program.schema.json", default_version);
let message_path = format!("schemas/message/{}/message.schema.json", default_version);
let eval_path = format!("schemas/eval/{}/eval.schema.json", default_version);
let embedding_path = format!(
"schemas/components/embedding/{}/embedding.schema.json",
default_version
);
let agentstate_path = format!(
"schemas/agentstate/{}/agentstate.schema.json",
default_version
);
let commitment_path = format!(
"schemas/commitment/{}/commitment.schema.json",
default_version
);
let todo_path = format!("schemas/todo/{}/todo.schema.json", default_version);
#[cfg(feature = "attestation")]
let attestation_path = format!(
"schemas/attestation/{}/attestation.schema.json",
default_version
);
let get_schema = |path: &str| -> Result<&str, JacsError> {
DEFAULT_SCHEMA_STRINGS
.get(path)
.copied()
.ok_or_else(|| JacsError::SchemaError(format!("Schema not found: {}", path)))
};
let headerdata = get_schema(&header_path)?;
let agentdata = get_schema(&agentversion_path)?;
let agreementdata = get_schema(&agreementversion_path)?;
let signaturedata = get_schema(&signatureversion_path)?;
let servicedata = get_schema(&service_path)?;
let unitdata = get_schema(&unit_path)?;
let actiondata = get_schema(&action_path)?;
let tooldata = get_schema(&tool_path)?;
let contactdata = get_schema(&contact_path)?;
let taskdata = get_schema(&task_path)?;
let messagedata = get_schema(&message_path)?;
let evaldata = get_schema(&eval_path)?;
let programdata = get_schema(&program_path)?;
let nodedata = get_schema(&node_path)?;
let embeddingdata = get_schema(&embedding_path)?;
let agentstatedata = get_schema(&agentstate_path)?;
let commitmentdata = get_schema(&commitment_path)?;
let tododata = get_schema(&todo_path)?;
#[cfg(feature = "attestation")]
let attestationdata = get_schema(&attestation_path)?;
let agentschema_result: Value = serde_json::from_str(agentdata)?;
let headerchema_result: Value = serde_json::from_str(headerdata)?;
let agreementschema_result: Value = serde_json::from_str(agreementdata)?;
let signatureschema_result: Value = serde_json::from_str(signaturedata)?;
let jacsconfigschema_result: Value = serde_json::from_str(CONFIG_SCHEMA_STRING)?;
let serviceschema_result: Value = serde_json::from_str(servicedata)?;
let unitschema_result: Value = serde_json::from_str(unitdata)?;
let actionschema_result: Value = serde_json::from_str(actiondata)?;
let toolschema_result: Value = serde_json::from_str(tooldata)?;
let contactschema_result: Value = serde_json::from_str(contactdata)?;
let taskschema_result: Value = serde_json::from_str(taskdata)?;
let messageschema_result: Value = serde_json::from_str(messagedata)?;
let evalschema_result: Value = serde_json::from_str(evaldata)?;
let nodeschema_result: Value = serde_json::from_str(nodedata)?;
let programschema_result: Value = serde_json::from_str(programdata)?;
let embeddingschema_result: Value = serde_json::from_str(embeddingdata)?;
let agentstateschema_result: Value = serde_json::from_str(agentstatedata)?;
let commitmentschema_result: Value = serde_json::from_str(commitmentdata)?;
let todoschema_result: Value = serde_json::from_str(tododata)?;
#[cfg(feature = "attestation")]
let attestationschema_result: Value = serde_json::from_str(attestationdata)?;
let agentschema = build_validator(&agentschema_result, &agentversion_path)?;
let headerschema = build_validator(&headerchema_result, &header_path)?;
let signatureschema = build_validator(&signatureschema_result, &signatureversion_path)?;
let jacsconfigschema = build_validator(&jacsconfigschema_result, "jacsconfigschema")?;
let serviceschema = build_validator(&serviceschema_result, &service_path)?;
let unitschema = build_validator(&unitschema_result, &unit_path)?;
let actionschema = build_validator(&actionschema_result, &action_path)?;
let toolschema = build_validator(&toolschema_result, &tool_path)?;
let agreementschema = build_validator(&agreementschema_result, &agreementversion_path)?;
let evalschema = build_validator(&evalschema_result, &eval_path)?;
let nodeschema = build_validator(&nodeschema_result, &node_path)?;
let programschema = build_validator(&programschema_result, &program_path)?;
let embeddingschema = build_validator(&embeddingschema_result, &embedding_path)?;
let contactschema = build_validator(&contactschema_result, &contact_path)?;
let taskschema = build_validator(&taskschema_result, &task_path)?;
let messageschema = build_validator(&messageschema_result, &message_path)?;
let agentstateschema = build_validator(&agentstateschema_result, &agentstate_path)?;
let commitmentschema = build_validator(&commitmentschema_result, &commitment_path)?;
let todoschema = build_validator(&todoschema_result, &todo_path)?;
#[cfg(feature = "attestation")]
let attestationschema = build_validator(&attestationschema_result, &attestation_path)?;
Ok(Self {
headerschema,
headerversion: headerversion.to_string(),
agentschema,
signatureschema,
jacsconfigschema,
agreementschema,
serviceschema,
unitschema,
actionschema,
toolschema,
contactschema,
taskschema,
messageschema,
evalschema,
nodeschema,
programschema,
embeddingschema,
agentstateschema,
commitmentschema,
todoschema,
#[cfg(feature = "attestation")]
attestationschema,
})
}
pub fn validate_header(&self, json: &str) -> Result<Value, JacsError> {
self.validate_json_with_schema(
json,
&self.headerschema,
"header.schema.json",
"Invalid JSON",
)
}
pub fn validate_task(&self, json: &str) -> Result<Value, JacsError> {
self.validate_json_with_schema(json, &self.taskschema, "task.schema.json", "Invalid JSON")
}
pub fn validate_agentstate(&self, json: &str) -> Result<Value, JacsError> {
self.validate_json_with_schema(
json,
&self.agentstateschema,
"agentstate.schema.json",
"Invalid JSON",
)
}
pub fn validate_todo(&self, json: &str) -> Result<Value, JacsError> {
self.validate_json_with_schema(json, &self.todoschema, "todo.schema.json", "Invalid JSON")
}
#[cfg(feature = "attestation")]
pub fn validate_attestation(&self, json: &str) -> Result<Value, JacsError> {
self.validate_json_with_schema(
json,
&self.attestationschema,
"attestation.schema.json",
"Invalid JSON",
)
}
pub fn validate_commitment(&self, json: &str) -> Result<Value, JacsError> {
self.validate_json_with_schema(
json,
&self.commitmentschema,
"commitment.schema.json",
"Invalid JSON",
)
}
pub fn validate_signature(&self, signature: &Value) -> Result<(), JacsError> {
let validation_result = self.signatureschema.validate(signature);
match validation_result {
Ok(_) => Ok(()),
Err(error) => {
let error_message =
format_schema_validation_error(&error, "signature.schema.json", signature);
error!("{}", error_message);
Err(JacsError::SchemaError(error_message))
}
}
}
pub fn validate_agent(&self, json: &str) -> Result<Value, JacsError> {
self.validate_json_with_schema(
json,
&self.agentschema,
"agent.schema.json",
"Invalid JSON for agent",
)
}
pub fn get_header_schema_url(&self) -> String {
format!(
"https://hai.ai/schemas/header/{}/header.schema.json",
self.headerversion
)
}
pub fn getschema(&self, value: Value) -> Result<String, JacsError> {
let schemafield = "$schema";
if let Some(schema) = value.get(schemafield)
&& let Some(schema_str) = schema.as_str()
{
return Ok(schema_str.to_string());
}
Err(JacsError::SchemaError(
"Schema extraction failed: no schema in doc or schema is not a string".to_string(),
))
}
pub fn getshortschema(&self, value: Value) -> Result<String, JacsError> {
let longschema = self.getschema(value)?;
let re = Regex::new(r"/([^/]+)\.schema\.json$").map_err(|e| JacsError::Internal {
message: format!("Invalid regex pattern: {}", e),
})?;
if let Some(caps) = re.captures(&longschema)
&& let Some(matched) = caps.get(1)
{
return Ok(matched.as_str().to_string());
}
Err(JacsError::SchemaError(
"Failed to extract schema name from URL".to_string(),
))
}
pub fn create(&self, json: &str) -> Result<Value, JacsError> {
let mut instance: serde_json::Value = match serde_json::from_str(json) {
Ok(value) => {
debug!("validate json {:?}", value);
value
}
Err(e) => {
let error_message = format!("Invalid JSON: {}", e);
error!("loading error {:?}", error_message);
return Err(e.into());
}
};
if instance.get_str("jacsId").is_some() || instance.get_str("jacsVersion").is_some() {
let error_message = "New JACs documents should have no id or version";
error!("{}", error_message);
return Err(error_message.into());
}
let id = Uuid::new_v4().to_string();
let version = Uuid::new_v4().to_string();
let original_version = version.clone();
let versioncreated = time_utils::now_rfc3339();
instance["jacsId"] = json!(format!("{}", id));
instance["jacsVersion"] = json!(format!("{}", version));
instance["jacsVersionDate"] = json!(format!("{}", versioncreated));
instance["jacsOriginalVersion"] = json!(format!("{}", original_version));
instance["jacsOriginalDate"] = json!(format!("{}", versioncreated));
instance["jacsLevel"] = json!(
instance
.get_str("jacsLevel")
.unwrap_or(DEFAULT_JACS_DOC_LEVEL.to_string())
);
if instance.get_str("$schema").is_none() {
instance["$schema"] = json!(format!("{}", self.get_header_schema_url()));
}
if instance.get_str("jacsType").is_none() {
let cloned_instance = instance.clone();
instance["jacsType"] = match self.getshortschema(cloned_instance) {
Ok(schema) => json!(schema),
Err(_) => json!("document"),
};
}
let validation_result = self.headerschema.validate(&instance);
match validation_result {
Ok(instance) => instance,
Err(error) => {
let schema_name = instance
.get("$schema")
.and_then(|v| v.as_str())
.unwrap_or("header.schema.json");
let error_message = format!(
"Document creation failed: {}",
format_schema_validation_error(&error, schema_name, &instance)
);
error!("{}", error_message);
return Err(JacsError::ValidationError(error_message));
}
};
Ok(instance.clone())
}
}
#[cfg(test)]
mod tests {
use super::Schema;
fn build_schema() -> Schema {
Schema::new("v1", "v1", "v1").expect("schema initialization should succeed")
}
#[test]
fn validate_agent_invalid_json_has_agent_context() {
let schema = build_schema();
let err = schema
.validate_agent("{not-json")
.expect_err("invalid JSON should fail");
assert!(
err.to_string().contains("Invalid JSON for agent"),
"expected agent-specific parse error"
);
}
#[test]
fn validate_task_invalid_json_has_generic_context() {
let schema = build_schema();
let err = schema
.validate_task("{not-json")
.expect_err("invalid JSON should fail");
assert!(
err.to_string().contains("Invalid JSON:"),
"expected generic parse error"
);
}
}