From 70fb27b8e8c61ce229001ab79d94a31edbacbb1e Mon Sep 17 00:00:00 2001 From: Jonathan Hendler Date: Tue, 24 Mar 2026 13:11:22 -0700 Subject: [PATCH 01/11] bump version --- CHANGELOG.md | 4 ++++ binding-core/Cargo.toml | 4 ++-- jacs-cli/Cargo.toml | 6 +++--- jacs-duckdb/Cargo.toml | 4 ++-- jacs-mcp/Cargo.toml | 6 +++--- jacs-mcp/contract/jacs-mcp-contract.json | 2 +- jacs-postgresql/Cargo.toml | 4 ++-- jacs-redb/Cargo.toml | 4 ++-- jacs-surrealdb/Cargo.toml | 4 ++-- jacs/Cargo.toml | 2 +- jacsgo/lib/Cargo.toml | 2 +- jacsnpm/Cargo.toml | 2 +- jacsnpm/package.json | 2 +- jacspy/Cargo.toml | 2 +- jacspy/pyproject.toml | 2 +- 15 files changed, 27 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 19d4bc1e..2d127edb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.9.12 + +(unreleased) + ## 0.9.11 (unreleased) diff --git a/binding-core/Cargo.toml b/binding-core/Cargo.toml index 23a23801..1eec564a 100644 --- a/binding-core/Cargo.toml +++ b/binding-core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "jacs-binding-core" -version = "0.9.11" +version = "0.9.12" edition = "2024" rust-version = "1.93" resolver = "3" @@ -19,7 +19,7 @@ attestation = ["jacs/attestation"] pq-tests = [] [dependencies] -jacs = { version = "0.9.11", path = "../jacs" } +jacs = { version = "0.9.12", path = "../jacs" } serde_json = "1.0" base64 = "0.22.1" serde = { version = "1.0", features = ["derive"] } diff --git a/jacs-cli/Cargo.toml b/jacs-cli/Cargo.toml index 6fa04086..c4acf0d0 100644 --- a/jacs-cli/Cargo.toml +++ b/jacs-cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "jacs-cli" -version = "0.9.11" +version = "0.9.12" edition = "2024" rust-version = "1.93" description = "JACS CLI: command-line interface for JSON AI Communication Standard" @@ -23,8 +23,8 @@ attestation = ["jacs/attestation"] keychain = ["jacs/keychain"] [dependencies] -jacs = { version = "0.9.11", path = "../jacs" } -jacs-mcp = { version = "0.9.11", path = "../jacs-mcp", features = ["mcp", "full-tools"], optional = true } +jacs = { version = "0.9.12", path = "../jacs" } +jacs-mcp = { version = "0.9.12", path = "../jacs-mcp", features = ["mcp", "full-tools"], optional = true } clap = { version = "4.5.4", features = ["derive", "cargo"] } rpassword = "7.3.1" reqwest = { version = "0.13.2", default-features = false, features = ["blocking", "json", "rustls"] } diff --git a/jacs-duckdb/Cargo.toml b/jacs-duckdb/Cargo.toml index 0f2dc53a..ed95b27b 100644 --- a/jacs-duckdb/Cargo.toml +++ b/jacs-duckdb/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "jacs-duckdb" -version = "0.1.5" +version = "0.1.6" edition = "2024" rust-version.workspace = true description = "DuckDB storage backend for JACS documents" @@ -13,7 +13,7 @@ keywords = ["cryptography", "json", "duckdb", "storage"] categories = ["database", "data-structures"] [dependencies] -jacs = { version = "0.9.11", path = "../jacs", default-features = false } +jacs = { version = "0.9.12", path = "../jacs", default-features = false } duckdb = { version = "1.4", features = ["bundled", "json"] } serde_json = "1.0" diff --git a/jacs-mcp/Cargo.toml b/jacs-mcp/Cargo.toml index f10bb989..b0603e16 100644 --- a/jacs-mcp/Cargo.toml +++ b/jacs-mcp/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "jacs-mcp" -version = "0.9.11" +version = "0.9.12" edition = "2024" rust-version = "1.93" description = "MCP server for JACS: data provenance and cryptographic signing of agent state" @@ -45,8 +45,8 @@ tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["fmt", "env-filter"] } rmcp = { version = "0.12", features = ["client", "server", "transport-io", "transport-child-process", "macros"], optional = true } tokio = { version = "1", features = ["rt-multi-thread", "macros", "process", "time"], optional = true } -jacs = { version = "0.9.11", path = "../jacs", default-features = true } -jacs-binding-core = { version = "0.9.11", path = "../binding-core", features = ["a2a"] } +jacs = { version = "0.9.12", path = "../jacs", default-features = true } +jacs-binding-core = { version = "0.9.12", path = "../binding-core", features = ["a2a"] } serde = { version = "1", features = ["derive"] } serde_json = "1" schemars = "1.0" diff --git a/jacs-mcp/contract/jacs-mcp-contract.json b/jacs-mcp/contract/jacs-mcp-contract.json index b3fb3935..357a73b9 100644 --- a/jacs-mcp/contract/jacs-mcp-contract.json +++ b/jacs-mcp/contract/jacs-mcp-contract.json @@ -3,7 +3,7 @@ "server": { "name": "jacs-mcp", "title": "JACS MCP Server", - "version": "0.9.11", + "version": "0.9.12", "website_url": "https://humanassisted.github.io/JACS/", "instructions": "This MCP server provides data provenance and cryptographic signing for agent state files and agent-to-agent messaging. Agent state tools: jacs_sign_state (sign files), jacs_verify_state (verify integrity), jacs_load_state (load with verification), jacs_update_state (update and re-sign), jacs_list_state (list signed docs), jacs_adopt_state (adopt external files). Memory tools: jacs_memory_save (save a memory), jacs_memory_recall (search memories by query), jacs_memory_list (list all memories), jacs_memory_forget (soft-delete a memory), jacs_memory_update (update an existing memory). Messaging tools: jacs_message_send (create and sign a message), jacs_message_update (update and re-sign a message), jacs_message_agree (co-sign/agree to a message), jacs_message_receive (verify and extract a received message). Agent management: jacs_create_agent (create new agent with keys), jacs_reencrypt_key (rotate private key password). A2A artifacts: jacs_wrap_a2a_artifact (sign artifact with provenance), jacs_verify_a2a_artifact (verify wrapped artifact), jacs_assess_a2a_agent (assess remote agent trust level). A2A discovery: jacs_export_agent_card (export Agent Card), jacs_generate_well_known (generate .well-known documents), jacs_export_agent (export full agent JSON). Trust store: jacs_trust_agent (add agent to trust store), jacs_untrust_agent (remove from trust store, requires JACS_MCP_ALLOW_UNTRUST=true), jacs_list_trusted_agents (list all trusted agent IDs), jacs_is_trusted (check if agent is trusted), jacs_get_trusted_agent (get trusted agent JSON). Attestation: jacs_attest_create (create signed attestation with claims), jacs_attest_verify (verify attestation, optionally with evidence checks), jacs_attest_lift (lift signed document into attestation), jacs_attest_export_dsse (export attestation as DSSE envelope). Security: jacs_audit (read-only security audit and health checks). Audit trail: jacs_audit_log (record events as signed audit entries), jacs_audit_query (search audit trail by action, target, time range), jacs_audit_export (export audit trail as signed bundle). Search: jacs_search (unified search across all signed documents)." }, diff --git a/jacs-postgresql/Cargo.toml b/jacs-postgresql/Cargo.toml index 19dd6ac0..0ed9fcd1 100644 --- a/jacs-postgresql/Cargo.toml +++ b/jacs-postgresql/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "jacs-postgresql" -version = "0.1.5" +version = "0.1.6" edition = "2024" rust-version.workspace = true description = "PostgreSQL storage backend for JACS documents" @@ -13,7 +13,7 @@ keywords = ["cryptography", "json", "postgresql", "storage"] categories = ["database", "data-structures"] [dependencies] -jacs = { version = "0.9.11", path = "../jacs", default-features = false } +jacs = { version = "0.9.12", path = "../jacs", default-features = false } sqlx = { version = "0.8.6", default-features = false, features = ["runtime-tokio-rustls", "postgres"] } tokio = { version = "1.0", features = ["rt-multi-thread"] } serde_json = "1.0" diff --git a/jacs-redb/Cargo.toml b/jacs-redb/Cargo.toml index 6e050bb2..896b093b 100644 --- a/jacs-redb/Cargo.toml +++ b/jacs-redb/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "jacs-redb" -version = "0.1.5" +version = "0.1.6" edition = "2024" rust-version.workspace = true readme.workspace = true @@ -13,7 +13,7 @@ categories.workspace = true description = "Redb (pure-Rust embedded KV) storage backend for JACS documents" [dependencies] -jacs = { version = "0.9.11", path = "../jacs", default-features = false } +jacs = { version = "0.9.12", path = "../jacs", default-features = false } redb = "3.1" chrono = "0.4.40" serde_json = "1.0" diff --git a/jacs-surrealdb/Cargo.toml b/jacs-surrealdb/Cargo.toml index ee47540e..ba765283 100644 --- a/jacs-surrealdb/Cargo.toml +++ b/jacs-surrealdb/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "jacs-surrealdb" -version = "0.1.5" +version = "0.1.6" edition = "2024" rust-version.workspace = true description = "SurrealDB storage backend for JACS documents" @@ -13,7 +13,7 @@ keywords = ["cryptography", "json", "surrealdb", "storage"] categories = ["database", "data-structures"] [dependencies] -jacs = { version = "0.9.11", path = "../jacs", default-features = false } +jacs = { version = "0.9.12", path = "../jacs", default-features = false } surrealdb = { version = "3.0.2", default-features = false, features = ["kv-mem"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" diff --git a/jacs/Cargo.toml b/jacs/Cargo.toml index e874f8a1..0e9b6473 100644 --- a/jacs/Cargo.toml +++ b/jacs/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "jacs" -version = "0.9.11" +version = "0.9.12" edition = "2024" rust-version = "1.93" resolver = "3" diff --git a/jacsgo/lib/Cargo.toml b/jacsgo/lib/Cargo.toml index 7dc3c387..af90641c 100644 --- a/jacsgo/lib/Cargo.toml +++ b/jacsgo/lib/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "jacsgo" -version = "0.9.11" +version = "0.9.12" edition = "2024" rust-version = "1.93" resolver = "3" diff --git a/jacsnpm/Cargo.toml b/jacsnpm/Cargo.toml index 14eccf41..81f79829 100644 --- a/jacsnpm/Cargo.toml +++ b/jacsnpm/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "jacsnpm" -version = "0.9.11" +version = "0.9.12" edition = "2024" rust-version = "1.93" resolver = "3" diff --git a/jacsnpm/package.json b/jacsnpm/package.json index e07ca105..a6a7e33b 100644 --- a/jacsnpm/package.json +++ b/jacsnpm/package.json @@ -1,6 +1,6 @@ { "name": "@hai.ai/jacs", - "version": "0.9.11", + "version": "0.9.12", "description": "JACS (JSON Agent Communication Standard) - Data provenance and cryptographic signing for AI agents", "main": "index.js", "types": "index.d.ts", diff --git a/jacspy/Cargo.toml b/jacspy/Cargo.toml index eadeebb0..736bd5ce 100644 --- a/jacspy/Cargo.toml +++ b/jacspy/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "jacspy" -version = "0.9.11" +version = "0.9.12" edition = "2024" rust-version = "1.93" resolver = "3" diff --git a/jacspy/pyproject.toml b/jacspy/pyproject.toml index 5b9a1cba..c1d3ced5 100644 --- a/jacspy/pyproject.toml +++ b/jacspy/pyproject.toml @@ -3,7 +3,7 @@ requires = ["maturin>=1.0,<2.0"] build-backend = "maturin" [project] name = "jacs" -version = "0.9.11" +version = "0.9.12" description = "JACS - JSON AI Communication Standard: Cryptographic signing and verification for AI agents." readme = "README.md" requires-python = ">=3.10" From 0232b9964e6b9328f7ebb430c02b07fb02a05514 Mon Sep 17 00:00:00 2001 From: Jonathan Hendler Date: Tue, 24 Mar 2026 14:34:12 -0700 Subject: [PATCH 02/11] yaml and html --- binding-core/src/simple_wrapper.rs | 44 ++ jacs-cli/src/main.rs | 109 +++++ jacs/Cargo.toml | 1 + jacs/src/convert/html.rs | 569 ++++++++++++++++++++++++++ jacs/src/convert/mod.rs | 31 ++ jacs/src/convert/yaml.rs | 346 ++++++++++++++++ jacs/src/error.rs | 68 +++ jacs/src/lib.rs | 1 + jacs/src/simple/convert.rs | 49 +++ jacs/src/simple/mod.rs | 1 + jacs/tests/cli_convert_tests.rs | 188 +++++++++ jacs/tests/convert_html_tests.rs | 180 ++++++++ jacs/tests/convert_lifecycle_tests.rs | 179 ++++++++ jacs/tests/convert_yaml_tests.rs | 180 ++++++++ jacs/tests/simple_convert_tests.rs | 104 +++++ jacsnpm/src/lib.rs | 36 ++ jacsnpm/test/convert.test.js | 93 +++++ jacspy/src/lib.rs | 67 +++ jacspy/tests/test_convert.py | 91 ++++ 19 files changed, 2337 insertions(+) create mode 100644 jacs/src/convert/html.rs create mode 100644 jacs/src/convert/mod.rs create mode 100644 jacs/src/convert/yaml.rs create mode 100644 jacs/src/simple/convert.rs create mode 100644 jacs/tests/cli_convert_tests.rs create mode 100644 jacs/tests/convert_html_tests.rs create mode 100644 jacs/tests/convert_lifecycle_tests.rs create mode 100644 jacs/tests/convert_yaml_tests.rs create mode 100644 jacs/tests/simple_convert_tests.rs create mode 100644 jacsnpm/test/convert.test.js create mode 100644 jacspy/tests/test_convert.py diff --git a/binding-core/src/simple_wrapper.rs b/binding-core/src/simple_wrapper.rs index bf20af2f..a90784b1 100644 --- a/binding-core/src/simple_wrapper.rs +++ b/binding-core/src/simple_wrapper.rs @@ -302,6 +302,50 @@ impl SimpleAgentWrapper { .map_err(|e| BindingCoreError::signing_failed(format!("Sign file failed: {}", e)))?; Ok(signed.raw) } + + // ========================================================================= + // Format Conversion (stateless -- no agent lock needed) + // ========================================================================= + + /// Convert a JSON string to YAML. + pub fn to_yaml(&self, json_str: &str) -> BindingResult { + jacs::convert::jacs_to_yaml(json_str).map_err(|e| { + BindingCoreError::new( + crate::ErrorKind::SerializationFailed, + format!("to_yaml failed: {}", e), + ) + }) + } + + /// Convert a YAML string to pretty-printed JSON. + pub fn from_yaml(&self, yaml_str: &str) -> BindingResult { + jacs::convert::yaml_to_jacs(yaml_str).map_err(|e| { + BindingCoreError::new( + crate::ErrorKind::SerializationFailed, + format!("from_yaml failed: {}", e), + ) + }) + } + + /// Convert a JSON string to a self-contained HTML document. + pub fn to_html(&self, json_str: &str) -> BindingResult { + jacs::convert::jacs_to_html(json_str).map_err(|e| { + BindingCoreError::new( + crate::ErrorKind::SerializationFailed, + format!("to_html failed: {}", e), + ) + }) + } + + /// Extract JSON from an HTML document produced by `to_html`. + pub fn from_html(&self, html_str: &str) -> BindingResult { + jacs::convert::html_to_jacs(html_str).map_err(|e| { + BindingCoreError::new( + crate::ErrorKind::SerializationFailed, + format!("from_html failed: {}", e), + ) + }) + } } // ============================================================================= diff --git a/jacs-cli/src/main.rs b/jacs-cli/src/main.rs index b434eb79..888c0854 100644 --- a/jacs-cli/src/main.rs +++ b/jacs-cli/src/main.rs @@ -1159,6 +1159,40 @@ pub fn main() -> Result<(), Box> { .arg_required_else_help(true), ); + let matches = matches + .subcommand( + Command::new("convert") + .about("Convert JACS documents between JSON, YAML, and HTML formats (no agent required)") + .arg( + Arg::new("to") + .long("to") + .required(true) + .value_parser(["json", "yaml", "html"]) + .help("Target format: json, yaml, or html"), + ) + .arg( + Arg::new("from") + .long("from") + .value_parser(["json", "yaml", "html"]) + .help("Source format (auto-detected from extension if omitted)"), + ) + .arg( + Arg::new("file") + .short('f') + .long("file") + .required(true) + .value_parser(value_parser!(String)) + .help("Input file path"), + ) + .arg( + Arg::new("output") + .short('o') + .long("output") + .value_parser(value_parser!(String)) + .help("Output file path (defaults to stdout)"), + ), + ); + let matches = matches.arg_required_else_help(true).get_matches(); match matches.subcommand() { @@ -2550,6 +2584,81 @@ pub fn main() -> Result<(), Box> { } } } + Some(("convert", convert_matches)) => { + use jacs::convert::{html_to_jacs, jacs_to_html, jacs_to_yaml, yaml_to_jacs}; + + let target_format = convert_matches.get_one::("to").unwrap(); + let source_format = convert_matches.get_one::("from"); + let file_path = convert_matches.get_one::("file").unwrap(); + let output_path = convert_matches.get_one::("output"); + + // Auto-detect source format from extension if not explicitly provided + let detected_format = if let Some(fmt) = source_format { + fmt.clone() + } else { + let ext = std::path::Path::new(file_path) + .extension() + .and_then(|e| e.to_str()) + .unwrap_or(""); + match ext { + "json" => "json".to_string(), + "yaml" | "yml" => "yaml".to_string(), + "html" | "htm" => "html".to_string(), + _ => { + eprintln!( + "Cannot auto-detect format for extension '{}'. Use --from to specify.", + ext + ); + process::exit(1); + } + } + }; + + // Read input + let input = std::fs::read_to_string(file_path) + .map_err(|e| format!("Failed to read '{}': {}", file_path, e))?; + + // Convert + let output = match (detected_format.as_str(), target_format.as_str()) { + ("json", "yaml") => jacs_to_yaml(&input) + .map_err(|e| format!("{}", e))?, + ("yaml", "json") => yaml_to_jacs(&input) + .map_err(|e| format!("{}", e))?, + ("json", "html") => jacs_to_html(&input) + .map_err(|e| format!("{}", e))?, + ("html", "json") => html_to_jacs(&input) + .map_err(|e| format!("{}", e))?, + ("yaml", "html") => { + let json = yaml_to_jacs(&input) + .map_err(|e| format!("{}", e))?; + jacs_to_html(&json) + .map_err(|e| format!("{}", e))? + } + ("html", "yaml") => { + let json = html_to_jacs(&input) + .map_err(|e| format!("{}", e))?; + jacs_to_yaml(&json) + .map_err(|e| format!("{}", e))? + } + (src, dst) if src == dst => { + // Same format -- just pass through + input + } + (src, dst) => { + eprintln!("Unsupported conversion: {} -> {}", src, dst); + process::exit(1); + } + }; + + // Write output + if let Some(out_path) = output_path { + std::fs::write(out_path, &output) + .map_err(|e| format!("Failed to write '{}': {}", out_path, e))?; + eprintln!("Written to {}", out_path); + } else { + print!("{}", output); + } + } Some(("init", init_matches)) => { let auto_yes = *init_matches.get_one::("yes").unwrap_or(&false); println!("--- Running Config Creation ---"); diff --git a/jacs/Cargo.toml b/jacs/Cargo.toml index 0e9b6473..88f43a2b 100644 --- a/jacs/Cargo.toml +++ b/jacs/Cargo.toml @@ -95,6 +95,7 @@ hickory-resolver = { version = "0.24", features = ["dnssec-ring"] } hkdf = "0.12" zeroize = "1.8" pbkdf2 = { version = "0.12", features = ["simple"] } +serde_yaml_ng = "0.10" tracing = "0.1" diff --git a/jacs/src/convert/html.rs b/jacs/src/convert/html.rs new file mode 100644 index 00000000..5cb1f5a6 --- /dev/null +++ b/jacs/src/convert/html.rs @@ -0,0 +1,569 @@ +//! JSON <-> HTML conversion for JACS documents. +//! +//! Provides conversion between JSON and self-contained HTML documents. The +//! HTML embeds the exact JSON in a ` + +"#, + title = title, + metadata_section = metadata_section, + content_section = content_section, + files_section = files_section, + json_data = json_str, + ); + + Ok(html) +} + +/// Extract JSON from an HTML document that was produced by [`jacs_to_html`]. +/// +/// Looks for a `"; + + let start = html_str.find(open_tag).ok_or_else(|| { + JacsError::conversion( + "HTML", + "JSON", + "no tag after our opening tag + let json_end = html_str[json_start..].find(close_tag).ok_or_else(|| { + JacsError::conversion( + "HTML", + "JSON", + "found opening jacs-data script tag but no closing tag", + ) + })?; + + let json_str = &html_str[json_start..json_start + json_end]; + + // Validate it is actually JSON + let _: serde_json::Value = serde_json::from_str(json_str).map_err(|e| { + JacsError::conversion( + "HTML", + "JSON", + format!("embedded JSON in script tag is malformed: {}", e), + ) + })?; + + Ok(json_str.to_string()) +} + +/// Escape special HTML characters in a string. +fn html_escape(s: &str) -> String { + s.replace('&', "&") + .replace('<', "<") + .replace('>', ">") + .replace('"', """) + .replace('\'', "'") +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn html_contains_doctype() { + let json = r#"{"hello": "world"}"#; + let html = jacs_to_html(json).unwrap(); + assert!( + html.starts_with(""), + "HTML should start with DOCTYPE" + ); + } + + #[test] + fn html_contains_embedded_json() { + let json = r#"{"hello": "world"}"#; + let html = jacs_to_html(json).unwrap(); + assert!( + html.contains(r#""#; + let result = html_to_jacs(html); + assert!(result.is_err()); + let msg = result.unwrap_err().to_string(); + assert!( + msg.contains("malformed"), + "Should mention malformed JSON: {}", + msg + ); + } + + #[test] + fn html_to_jacs_from_non_html_returns_error() { + let result = html_to_jacs("just plain text, not html at all"); + assert!(result.is_err()); + } + + #[test] + fn html_output_is_self_contained() { + let json = r#"{"hello": "world"}"#; + let html = jacs_to_html(json).unwrap(); + assert!( + !html.contains(r#""), "HTML should have "); + assert!(html.contains(""), "HTML should have "); + assert!(html.contains(""), "HTML should have "); + assert!(html.contains(""), "HTML should have "); + } + + #[test] + fn html_has_charset_utf8() { + let json = r#"{"test": true}"#; + let html = jacs_to_html(json).unwrap(); + assert!( + html.contains("charset=\"UTF-8\"") || html.contains("charset=UTF-8"), + "HTML should declare UTF-8 charset" + ); + } + + #[test] + fn html_inline_css_no_external_links() { + let json = r#"{"test": true}"#; + let html = jacs_to_html(json).unwrap(); + assert!( + html.contains("