#![allow(deprecated)]
use crate::agent::boilerplate::BoilerPlate;
use crate::config::{Config, check_env_vars, set_env_vars};
use crate::create_minimal_blank_agent;
use crate::crypt::KeyManager;
use crate::dns::bootstrap as dns_bootstrap;
use crate::error::JacsError;
use crate::get_empty_agent;
use crate::storage::MultiStorage;
use crate::storage::jenv::set_env_var;
use rpassword::read_password;
use serde_json::{Value, json};
use std::env;
use std::fs::File;
use std::io;
use std::io::Write;
use std::path::Path;
use std::process;
use crate::simple::{AgentInfo, CreateAgentParams, SimpleAgent};
const CLI_PASSWORD_FILE_ENV: &str = "JACS_PASSWORD_FILE";
pub fn handle_agent_create_programmatic(params: CreateAgentParams) -> Result<AgentInfo, JacsError> {
let (_agent, info) = SimpleAgent::create_with_params(params)?;
Ok(info)
}
fn request_string(message: &str, default: &str) -> String {
let mut input = String::new();
println!("{}: (default: {})", message, default);
match io::stdin().read_line(&mut input) {
Ok(_) => {
let trimmed = input.trim();
if trimmed.is_empty() {
default.to_string() } else {
trimmed.to_string() }
}
Err(_) => default.to_string(), }
}
fn resolve_cli_password_for_config_create() -> Result<Option<String>, JacsError> {
let env_password = match env::var("JACS_PRIVATE_KEY_PASSWORD") {
Ok(value) => {
if value.trim().is_empty() {
return Err(
"JACS_PRIVATE_KEY_PASSWORD is set but empty. Provide a non-empty password."
.into(),
);
}
Some(value)
}
Err(std::env::VarError::NotPresent) => None,
Err(std::env::VarError::NotUnicode(_)) => {
return Err("JACS_PRIVATE_KEY_PASSWORD contains non-UTF-8 data.".into());
}
};
let password_file = match env::var(CLI_PASSWORD_FILE_ENV) {
Ok(value) => {
if value.trim().is_empty() {
return Err(format!(
"{} is set but empty. Provide a non-empty file path.",
CLI_PASSWORD_FILE_ENV
)
.into());
}
Some(value)
}
Err(std::env::VarError::NotPresent) => None,
Err(std::env::VarError::NotUnicode(_)) => {
return Err(format!("{} contains non-UTF-8 data.", CLI_PASSWORD_FILE_ENV).into());
}
};
if env_password.is_some() && password_file.is_some() {
return Err(format!(
"Multiple password sources configured: JACS_PRIVATE_KEY_PASSWORD and {}. Configure exactly one.",
CLI_PASSWORD_FILE_ENV
)
.into());
}
if let Some(env_password) = env_password {
println!("Using password from JACS_PRIVATE_KEY_PASSWORD environment variable.");
return Ok(Some(env_password));
}
if let Some(path) = password_file {
let password_path = Path::new(path.trim());
let raw = std::fs::read_to_string(password_path).map_err(|e| {
format!(
"Failed to read {} at '{}': {}",
CLI_PASSWORD_FILE_ENV,
password_path.display(),
e
)
})?;
let password = raw.trim_end_matches(|c| c == '\n' || c == '\r').to_string();
if password.is_empty() {
return Err(format!(
"{} at '{}' is empty.",
CLI_PASSWORD_FILE_ENV,
password_path.display()
)
.into());
}
println!(
"Using password from {} ('{}').",
CLI_PASSWORD_FILE_ENV,
password_path.display()
);
return Ok(Some(password));
}
Ok(None)
}
pub fn handle_config_create() -> Result<(), JacsError> {
println!("Welcome to the JACS Config Generator!");
let storage: MultiStorage = MultiStorage::default_new().expect("Failed to initialize storage");
println!("Enter the path to the agent file if it already exists (leave empty to skip):");
let mut agent_filename = String::new();
io::stdin().read_line(&mut agent_filename).unwrap();
agent_filename = agent_filename.trim().to_string();
let jacs_agent_id_and_version = if !agent_filename.is_empty() {
match storage.file_exists(&agent_filename, None) {
Ok(true) => match storage.get_file(&agent_filename, None) {
Ok(agent_content_bytes) => match String::from_utf8(agent_content_bytes) {
Ok(agent_content) => match serde_json::from_str::<Value>(&agent_content) {
Ok(agent_json) => {
let jacs_id = agent_json["jacsId"].as_str().unwrap_or("");
let jacs_version = agent_json["jacsVersion"].as_str().unwrap_or("");
format!("{}:{}", jacs_id, jacs_version)
}
Err(e) => {
println!("Error parsing agent JSON from {}: {}", agent_filename, e);
String::new()
}
},
Err(e) => {
println!(
"Error converting agent file content to UTF-8 {}: {}",
agent_filename, e
);
String::new()
}
},
Err(e) => {
println!("Failed to read agent file {}: {}", agent_filename, e);
String::new()
}
},
Ok(false) => {
println!(
"Agent file {} not found in storage. Skipping...",
agent_filename
);
String::new()
}
Err(e) => {
println!(
"Error checking existence of agent file {}: {}",
agent_filename, e
);
String::new()
}
}
} else {
String::new()
};
let config_path = "jacs.config.json";
if Path::new(config_path).exists() {
println!(
"Configuration file '{}' already exists. Please remove or rename it if you want to create a new one.",
config_path
);
process::exit(0); }
let jacs_agent_private_key_filename =
request_string("Enter the private key filename:", "jacs.private.pem.enc");
let jacs_agent_public_key_filename =
request_string("Enter the public key filename:", "jacs.public.pem");
let jacs_agent_key_algorithm = request_string(
"Enter the agent key algorithm (pq2025, ring-Ed25519, or RSA-PSS)",
"pq2025",
);
let jacs_default_storage = request_string("Enter the default storage (fs, aws, hai)", "fs");
let jacs_private_key_password = match resolve_cli_password_for_config_create()? {
Some(password) => password,
None => {
println!(
"\nNo password source configured. Set exactly one non-interactive source:\n \
export JACS_PRIVATE_KEY_PASSWORD='your-strong-password'\n \
export {}=/path/to/password\n \
# or: export JACS_PRIVATE_KEY_PASSWORD=\"$(cat /path/to/password)\"\n",
CLI_PASSWORD_FILE_ENV
);
println!("{}", crate::crypt::aes_encrypt::password_requirements());
loop {
println!("Please enter a password (used to encrypt private key):");
let password = match read_password() {
Ok(pass) => pass,
Err(e) => {
eprintln!("Error reading password: {}. Please try again.", e);
continue;
}
};
if password.is_empty() {
eprintln!("Password cannot be empty. Please try again.");
continue;
}
println!("Please confirm the password:");
let password_confirm = match read_password() {
Ok(pass) => pass,
Err(e) => {
eprintln!(
"Error reading confirmation password: {}. Please start over.",
e
);
continue; }
};
if password == password_confirm {
break password; } else {
eprintln!("Passwords do not match. Please try again.");
}
}
}
};
let jacs_use_security = request_string("Use experimental security features", "false");
let jacs_data_directory = request_string("Directory for data storage", "./jacs");
let jacs_key_directory = request_string("Directory for keys", "./jacs_keys");
let jacs_agent_domain = request_string(
"Agent domain for DNSSEC fingerprint (optional, e.g., example.com)",
"",
);
let mut config = Config::new(
Some(jacs_use_security),
Some(jacs_data_directory),
Some(jacs_key_directory),
Some(jacs_agent_private_key_filename),
Some(jacs_agent_public_key_filename),
Some(jacs_agent_key_algorithm),
Some(jacs_private_key_password),
Some(jacs_agent_id_and_version),
Some(jacs_default_storage),
);
if !jacs_agent_domain.trim().is_empty() {
let mut v = serde_json::to_value(&config).unwrap_or(serde_json::json!({}));
if let Some(obj) = v.as_object_mut() {
obj.insert(
"jacs_agent_domain".to_string(),
serde_json::Value::String(jacs_agent_domain.trim().to_string()),
);
}
config = serde_json::from_value(v).unwrap_or(config);
}
let mut value = serde_json::to_value(&config).unwrap_or(serde_json::json!({}));
if let Some(obj) = value.as_object_mut() {
if obj.get("jacs_agent_domain").is_some_and(|v| v.is_null()) {
obj.remove("jacs_agent_domain");
}
}
let serialized = serde_json::to_string_pretty(&value).unwrap();
let mut file = File::create(config_path)
.map_err(|e| format!("Failed to create config file '{}': {}", config_path, e))?;
file.write_all(serialized.as_bytes())
.map_err(|e| format!("Failed to write to config file '{}': {}", config_path, e))?;
println!("jacs.config.json file generated successfully!");
Ok(())
}
pub fn handle_agent_create(filename: Option<&String>, create_keys: bool) -> Result<(), JacsError> {
handle_agent_create_inner(filename, create_keys, false)
}
pub fn handle_agent_create_auto(
filename: Option<&String>,
create_keys: bool,
auto_update_config: bool,
) -> Result<(), JacsError> {
handle_agent_create_inner(filename, create_keys, auto_update_config)
}
fn handle_agent_create_inner(
filename: Option<&String>,
create_keys: bool,
auto_update_config: bool,
) -> Result<(), JacsError> {
let storage: MultiStorage = MultiStorage::default_new().expect("Failed to initialize storage");
let config_path_str = "jacs.config.json";
let _ = if Path::new(config_path_str).exists() {
match std::fs::read_to_string(config_path_str) {
Ok(content) => {
println!("Loading configuration from {}...", config_path_str);
set_env_vars(false, Some(&content), false)
}
Err(e) => {
eprintln!("Warning: Could not read {}: {}", config_path_str, e);
set_env_vars(false, None, false)
}
}
} else {
println!(
"{} not found, proceeding with defaults or environment variables.",
config_path_str
);
set_env_vars(false, None, false)
};
let agent_type = request_string("Agent Type (e.g., ai, person, service, device)", "ai"); if agent_type.is_empty() {
eprintln!("Agent type cannot be empty.");
process::exit(1);
}
let service_description = request_string(
"Service Description",
"Describe a service the agent provides",
);
let success_description = request_string(
"Service Success Description",
"Describe a success of the service",
);
let failure_description = request_string(
"Service Failure Description",
"Describe what failure is of the service",
);
let (minimal_service_desc, minimal_success_desc, minimal_failure_desc) = if filename.is_none() {
(
Some(service_description),
Some(success_description),
Some(failure_description),
)
} else {
(None, None, None)
};
let agent_template_string = match filename {
Some(fname) => {
let content_bytes = storage
.get_file(fname, None)
.map_err(|e| format!("Failed to load agent template file '{}': {}", fname, e))?;
String::from_utf8(content_bytes)
.map_err(|e| format!("Agent template file {} is not valid UTF-8: {}", fname, e))?
}
_ => create_minimal_blank_agent(
agent_type.clone(), minimal_service_desc, minimal_success_desc, minimal_failure_desc, )
.map_err(|e| format!("Failed to create minimal agent template: {}", e))?,
};
let mut agent_json: Value = serde_json::from_str(&agent_template_string).map_err(|e| {
format!(
"Failed to parse agent template JSON: {}\nTemplate content:\n{}",
e, agent_template_string
)
})?;
if let Some(obj) = agent_json.as_object_mut() {
obj.insert("jacsAgentType".to_string(), json!(agent_type)); } else {
return Err("Agent template is not a valid JSON object.".into());
}
let modified_agent_string = serde_json::to_string(&agent_json)?;
let mut agent = get_empty_agent();
println!("Proceeding with agent creation using loaded configuration/environment variables.");
if create_keys {
println!("Creating keys...");
agent.generate_keys()?;
println!(
"Keys created in {}. Don't loose them! Keep them in a safe place. ",
agent
.config
.as_ref()
.unwrap()
.jacs_key_directory()
.as_deref()
.unwrap_or_default()
);
agent.set_dns_strict(false);
if let Some(domain) = agent
.config
.as_ref()
.and_then(|c| c.jacs_agent_domain().clone())
.filter(|s| !s.is_empty())
&& let Ok(pk) = agent.get_public_key()
{
let agent_id = agent.get_id().unwrap_or_else(|_| "".to_string());
let digest = dns_bootstrap::pubkey_digest_b64(&pk);
let rr = dns_bootstrap::build_dns_record(
&domain,
3600,
&agent_id,
&digest,
dns_bootstrap::DigestEncoding::Base64,
);
println!("\nDNS (BIND):\n{}\n", dns_bootstrap::emit_plain_bind(&rr));
println!(
"Use 'jacs agent dns --domain {} --provider <plain|aws|azure|cloudflare>' for provider-specific commands.",
domain
);
println!("Reminder: enable DNSSEC for the zone and publish DS at the registrar.");
}
}
agent.create_agent_and_load(&modified_agent_string, false, None)?;
let agent_id_version = agent.get_lookup_id()?;
println!("Agent {} created successfully!", agent_id_version);
agent.save()?;
let should_update = if auto_update_config {
true
} else {
let prompt_message = format!(
"Do you want to set {} as the default agent in jacs.config.json and environment variable? (yes/no)",
agent_id_version
);
let update_confirmation = request_string(&prompt_message, "no");
update_confirmation.trim().to_lowercase() == "yes"
|| update_confirmation.trim().to_lowercase() == "y"
};
if should_update {
println!("Updating configuration...");
let config_path_str = "jacs.config.json";
let config_path = Path::new(config_path_str);
let mut current_config: Value = match std::fs::read_to_string(config_path) {
Ok(content) => serde_json::from_str(&content).unwrap_or_else(|e| {
println!(
"Warning: Could not parse {}, creating default. Error: {}",
config_path_str, e
);
json!({}) }),
Err(ref e) if e.kind() == io::ErrorKind::NotFound => {
println!("Warning: {} not found, creating default.", config_path_str);
json!({}) }
Err(e) => {
eprintln!("Error reading {}: {}. Cannot update.", config_path_str, e);
return Ok(()); }
};
if !current_config.is_object() {
println!(
"Warning: {} content is not a JSON object. Overwriting with default structure.",
config_path_str
);
current_config = json!({});
}
if let Some(obj) = current_config.as_object_mut() {
obj.insert(
"jacs_agent_id_and_version".to_string(),
json!(agent_id_version),
);
if !obj.contains_key("$schema") {
obj.insert(
"$schema".to_string(),
json!("https://hai.ai/schemas/jacs.config.schema.json"),
);
}
}
match std::fs::write(
config_path,
serde_json::to_string_pretty(¤t_config).unwrap(),
) {
Ok(_) => println!("Successfully updated {}.", config_path_str),
Err(e) => eprintln!("Error writing {}: {}", config_path_str, e),
}
match set_env_var("JACS_AGENT_ID_AND_VERSION", &agent_id_version) {
Ok(_) => {
println!("Updated JACS_AGENT_ID_AND_VERSION environment variable for this session.")
}
Err(e) => eprintln!(
"Failed to update JACS_AGENT_ID_AND_VERSION environment variable: {}",
e
),
}
match check_env_vars(false) {
Ok(report) => println!("Environment Variable Check:\n{}", report),
Err(e) => {
eprintln!("Error checking environment variables after update: {}", e)
}
}
} else {
println!("Skipping configuration update.");
}
Ok(())
}