jacs 0.9.12

JACS JSON AI Communication Standard
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
use crate::agent::Agent;
use crate::agent::boilerplate::BoilerPlate;
use crate::config::{Config, check_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::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 crate::simple::{AgentInfo, CreateAgentParams, SimpleAgent};

const CLI_PASSWORD_FILE_ENV: &str = "JACS_PASSWORD_FILE";

/// Programmatic agent creation for non-interactive use.
///
/// Accepts pre-built `CreateAgentParams` and delegates to `SimpleAgent::create_with_params()`.
/// Use this when integrating CLI commands with the programmatic API.
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() // Return default if no input
            } else {
                trimmed.to_string() // Return trimmed input if there's any
            }
        }
        Err(_) => default.to_string(), // Return default on error
    }
}

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());
        }
    };

    // Env var takes priority. If both are set, warn but use env var.
    if let Some(env_password) = env_password {
        if password_file.is_some() {
            eprintln!(
                "Warning: both JACS_PRIVATE_KEY_PASSWORD and {} are set. \
                 Using JACS_PRIVATE_KEY_PASSWORD (highest priority).",
                CLI_PASSWORD_FILE_ENV
            );
        }
        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
            )
        })?;
        // Preserve intentional leading/trailing spaces in passphrases; strip only line endings.
        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));
    }

    // Note: OS keychain lookup requires an agent_id and is handled at agent
    // creation time, not during config creation.

    Ok(None)
}

// Function to handle the 'config create' logic
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() {
        // Use storage to check and read the agent file
        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()
    };

    // --- Check if config file already exists ---
    let config_path = "jacs.config.json";
    if Path::new(config_path).exists() {
        return Err(format!(
            "Configuration file '{}' already exists. Please remove or rename it if you want to create a new one.",
            config_path
        )
        .into());
    }
    // --- End check ---

    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");

    // Check for password in environment/bootstrap source first
    let jacs_private_key_password = match resolve_cli_password_for_config_create()? {
        Some(password) => password,
        None => {
            // No non-interactive source set; prompt user interactively.
            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; // Ask again from the beginning
                    }
                };

                if password == password_confirm {
                    break password; // Passwords match and are not empty, exit loop
                } else {
                    eprintln!("Passwords do not match. Please try again.");
                    // Loop continues
                }
            }
        }
    };

    // Note: keychain storage is deferred to agent creation, which has
    // the agent_id needed for per-agent keychain entries.

    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),
    );

    // insert optional domain if provided
    if !jacs_agent_domain.trim().is_empty() {
        // Serialize to Value, add field, then write
        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);
    }

    // Serialize, but ensure we omit any null fields that may have slipped through
    let mut value = serde_json::to_value(&config).unwrap_or(serde_json::json!({}));
    if let Some(obj) = value.as_object_mut() {
        // Remove optional domain if it ended up as null
        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();

    // Keep using std::fs for config file backup and writing
    // The check and backup logic below is no longer needed as we exit earlier if the file exists.
    /*
    let config_path = "jacs.config.json"; // This line is already defined above
    if metadata(config_path).is_ok() {
        // Keep std::fs::metadata
        let now: DateTime<Local> = Local::now();
        let backup_path = format!("{}-backup-jacs.config.json", now.format("%Y%m%d%H%M%S"));
        rename(config_path, backup_path.clone()).unwrap(); // Keep std::fs::rename
        println!("Backed up existing jacs.config.json to {}", backup_path);
    }
    */

    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(())
}

// Function to handle the 'agent create' logic
pub fn handle_agent_create(filename: Option<&String>, create_keys: bool) -> Result<(), JacsError> {
    handle_agent_create_inner(filename, create_keys, false)
}

/// Like `handle_agent_create` but when `auto_update_config` is true, automatically
/// sets the new agent ID in `jacs.config.json` without prompting.
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 mut agent = if Path::new(config_path_str).exists() {
        println!("Loading configuration from {}...", config_path_str);
        Agent::builder()
            .config_path(config_path_str)
            .build()
            .map_err(|e| {
                format!(
                    "Failed to initialize agent from config '{}': {}",
                    config_path_str, e
                )
            })?
    } else {
        println!(
            "{} not found, proceeding with defaults or environment variables.",
            config_path_str
        );
        Agent::builder()
            .build()
            .map_err(|e| format!("Failed to initialize agent with defaults: {}", e))?
    };

    // publish_to_env() removed: Agent now carries key_paths from config,
    // so FsEncryptedStore uses Agent.key_paths() instead of env reads.

    // -- Get user input for agent type and SERVICE descriptions --
    let agent_type = request_string("Agent Type (e.g., ai, person, service, device)", "ai"); // Default to ai
    if agent_type.is_empty() {
        return Err("Agent type cannot be empty.".into());
    }
    // TODO: Validate agent_type against schema enum: ["human", "human-org", "hybrid", "ai"]

    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",
    );

    // Variables for service descriptions when creating minimal agent
    let (minimal_service_desc, minimal_success_desc, minimal_failure_desc) = if filename.is_none() {
        // Use descriptions collected from user only if creating minimal agent
        (
            Some(service_description),
            Some(success_description),
            Some(failure_description),
        )
    } else {
        // If loading from file, pass None (template should contain service info)
        (None, None, None)
    };

    // TODO output instructions for updating agent definition

    // Load or create base agent string
    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(),   // Pass the collected agent_type
            minimal_service_desc, // Pass collected service description
            minimal_success_desc, // Pass collected success description
            minimal_failure_desc, // Pass collected failure description
        )
        .map_err(|e| format!("Failed to create minimal agent template: {}", e))?,
    };

    // -- Modify the agent template with remaining user input (agent_type) --
    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
        )
    })?;

    // Add or update fields - ONLY agent_type remains needed here as name/desc removed
    if let Some(obj) = agent_json.as_object_mut() {
        // obj.insert("jacsName".to_string(), json!(agent_name)); // Removed
        // obj.insert("jacsDescription".to_string(), json!(agent_description)); // Removed
        obj.insert("jacsAgentType".to_string(), json!(agent_type)); // Use jacsAgentType based on schema
    } else {
        return Err("Agent template is not a valid JSON object.".into());
    }

    let modified_agent_string = serde_json::to_string(&agent_json)?;

    // Proceed with agent creation using the already-initialized config-backed 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()
        );
        // If a domain is configured, emit DNS fingerprint instructions (non-strict at creation time)
        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.");
        }
    }

    // Use the modified agent string here
    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()?;

    // -- Determine whether to update the config --
    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);

        // Use std::fs for reading
        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!({}) // Start with empty object if parse fails or file empty
            }),
            Err(ref e) if e.kind() == io::ErrorKind::NotFound => {
                println!("Warning: {} not found, creating default.", config_path_str);
                json!({}) // Start with empty object if file doesn't exist
            }
            Err(e) => {
                eprintln!("Error reading {}: {}. Cannot update.", config_path_str, e);
                return Ok(()); // Exit this block gracefully if read fails for other reasons
            }
        };

        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"),
                );
            }
        }

        // Use std::fs for writing
        match std::fs::write(
            config_path,
            serde_json::to_string_pretty(&current_config).unwrap(),
        ) {
            Ok(_) => println!("Successfully updated {}.", config_path_str),
            Err(e) => eprintln!("Error writing {}: {}", config_path_str, e),
        }

        // Update environment variables for the current session
        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(())
}