#define _POSIX_C_SOURCE 200809L #include "setup_wizard.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include "cjson/cJSON.h" #include "llm.h" #include "main.h" #include "nostr_handler.h" #include "default_events.h" #include "../../nostr_core_lib/nostr_core/nostr_core.h" #define ANSI_UNDERLINE "\x1b[4m" #define ANSI_RESET "\x1b[0m" #define ANSI_YELLOW "\x1b[33m" #define ANSI_RED "\x1b[31m" #define WIZARD_LINE_MAX 1024 typedef struct { char key; const char* label; } wizard_option_t; static int is_hex64(const char* s) { if (!s || strlen(s) != 64U) return 0; for (size_t i = 0; i < 64U; i++) { if (!isxdigit((unsigned char)s[i])) return 0; } return 1; } static int decode_pubkey_hex_or_npub_local(const char* input, char out_hex[65]) { if (!input || !out_hex) return -1; if (strncmp(input, "npub1", 5) == 0) { unsigned char pubkey[32]; if (nostr_decode_npub(input, pubkey) != 0) { return -1; } nostr_bytes_to_hex(pubkey, 32, out_hex); return 0; } if (is_hex64(input)) { snprintf(out_hex, 65, "%s", input); return 0; } return -1; } static int derive_keys_from_nsec_local(const char* nsec_or_hex, didactyl_config_t* cfg) { if (!nsec_or_hex || !cfg) return -1; size_t n = strlen(nsec_or_hex); if (n == 0 || n >= sizeof(cfg->keys.nsec)) { return -1; } snprintf(cfg->keys.nsec, sizeof(cfg->keys.nsec), "%s", nsec_or_hex); int dec_rc = -1; if (strncmp(nsec_or_hex, "nsec1", 5) == 0) { dec_rc = (nostr_decode_nsec(nsec_or_hex, cfg->keys.private_key) == 0) ? 0 : -1; } else if (strlen(nsec_or_hex) == 64U) { dec_rc = (nostr_hex_to_bytes(nsec_or_hex, cfg->keys.private_key, 32) == 0) ? 0 : -1; } if (dec_rc != 0) { return -1; } if (nostr_ec_public_key_from_private_key(cfg->keys.private_key, cfg->keys.public_key) != 0) { return -1; } nostr_bytes_to_hex(cfg->keys.public_key, 32, cfg->keys.public_key_hex); return 0; } static int line_is_quit(const char* s) { if (!s) return 0; while (*s && isspace((unsigned char)*s)) s++; char c = (char)tolower((unsigned char)*s); return c == 'q' || c == 'x'; } static void trim_line(char* s) { if (!s) return; size_t n = strlen(s); while (n > 0 && (s[n - 1] == '\n' || s[n - 1] == '\r' || isspace((unsigned char)s[n - 1]))) { s[n - 1] = '\0'; n--; } char* p = s; while (*p && isspace((unsigned char)*p)) p++; if (p != s) { memmove(s, p, strlen(p) + 1U); } } static int read_line_prompt(const char* prompt, char* out, size_t out_size) { if (!out || out_size == 0) return -1; out[0] = '\0'; if (prompt && prompt[0] != '\0') { fprintf(stderr, "%s", prompt); fflush(stderr); } if (!fgets(out, (int)out_size, stdin)) { return -1; } trim_line(out); return 0; } static int read_secret_prompt(const char* prompt, char* out, size_t out_size) { if (!out || out_size == 0) return -1; out[0] = '\0'; struct termios oldt; struct termios newt; int disable_echo = 0; if (tcgetattr(STDIN_FILENO, &oldt) == 0) { newt = oldt; newt.c_lflag &= (tcflag_t)~ECHO; if (tcsetattr(STDIN_FILENO, TCSANOW, &newt) == 0) { disable_echo = 1; } } if (prompt && prompt[0] != '\0') { fprintf(stderr, "%s", prompt); fflush(stderr); } if (!fgets(out, (int)out_size, stdin)) { if (disable_echo) { tcsetattr(STDIN_FILENO, TCSANOW, &oldt); fprintf(stderr, "\n"); fflush(stderr); } return -1; } if (disable_echo) { tcsetattr(STDIN_FILENO, TCSANOW, &oldt); fprintf(stderr, "\n"); fflush(stderr); } trim_line(out); return 0; } static char read_menu_choice(const wizard_option_t* options, int count) { char line[WIZARD_LINE_MAX]; if (read_line_prompt("> ", line, sizeof(line)) != 0) { return 'q'; } if (line_is_quit(line)) { return 'q'; } char c = '\0'; for (size_t i = 0; line[i] != '\0'; i++) { if (!isspace((unsigned char)line[i])) { c = (char)tolower((unsigned char)line[i]); break; } } if (c == '\0') { return '\0'; } for (int i = 0; i < count; i++) { if ((char)tolower((unsigned char)options[i].key) == c) { return c; } } return '\0'; } static void print_option(char key, const char* tail) { fprintf(stderr, " %s%c%s%s\n", ANSI_UNDERLINE, (int)toupper((unsigned char)key), ANSI_RESET, tail ? tail : ""); } static int get_terminal_width(void) { struct winsize w; if (ioctl(STDERR_FILENO, TIOCGWINSZ, &w) == 0) { return w.ws_col; } return 80; // Fallback } static void print_centered(const char* s, int width) { int len = (int)strlen(s); if (len >= width) { fprintf(stderr, "%s\n", s); return; } int padding = (width - len) / 2; for (int i = 0; i < padding; i++) fputc(' ', stderr); fprintf(stderr, "%s\n", s); } static void print_full_width_line(char c, int width) { for (int i = 0; i < width; i++) fputc(c, stderr); fputc('\n', stderr); } static void print_interactive_splash(void) { int width = get_terminal_width(); fprintf(stderr, "\033[H\033[J\n\n\n\n"); const char* art[] = { "$$$$$$$\\ $$\\ $$\\ $$\\ $$\\ ", "$$ __$$\\ \\__| $$ | $$ | $$ |", "$$ | $$ |$$\\ $$$$$$$ | $$$$$$\\ $$$$$$$\\ $$$$$$\\ $$\\ $$\\ $$ |", "$$ | $$ |$$ |$$ __$$ | \\____$$\\ $$ _____|\\_$$ _| $$ | $$ |$$ |", "$$ | $$ |$$ |$$ / $$ | $$$$$$$ |$$ / $$ | $$ | $$ |$$ |", "$$ | $$ |$$ |$$ | $$ |$$ __$$ |$$ | $$ |$$\\ $$ | $$ |$$ |", "$$$$$$$ |$$ |\\$$$$$$$ |\\$$$$$$$ |\\$$$$$$$\\ \\$$$$ |\\$$$$$$$ |$$ |", "\\_______/ \\__| \\_______| \\_______| \\_______| \\____/ \\____$$ |\\__|", " $$\\ $$ | ", " \\$$$$$$ | ", " \\______/ " }; for (size_t i = 0; i < sizeof(art) / sizeof(art[0]); i++) { print_centered(art[i], width); } fprintf(stderr, "\n"); } static void render_wizard_page_header(const char* step_title, const char* section_title) { int width = get_terminal_width(); char title[256]; snprintf(title, sizeof(title), "Didactyl -- %s -- Interactive Setup", DIDACTYL_VERSION); print_interactive_splash(); print_full_width_line('=', width); print_full_width_line('=', width); print_centered(title, width); print_centered("Not your keys, not your agent", width); if (section_title && section_title[0] != '\0') { print_centered(section_title, width); } if (step_title && step_title[0] != '\0') { print_centered(step_title, width); } print_full_width_line('=', width); print_full_width_line('=', width); fprintf(stderr, "\n"); } static void config_set_defaults(didactyl_config_t* cfg) { if (!cfg) return; memset(cfg, 0, sizeof(*cfg)); cfg->dm_protocol = DM_PROTOCOL_NIP04; cfg->tools.enabled = 1; cfg->tools.max_turns = 20; cfg->tools.trigger_max_turns = 12; cfg->tools.api_default_max_turns = 8; cfg->tools.api_max_turns_ceiling = 32; cfg->tools.stall_repeat_threshold = 3; cfg->tools.local_http_fetch_default_timeout_seconds = 20; cfg->tools.local_http_fetch_max_timeout_seconds = 120; cfg->tools.shell.enabled = 1; cfg->tools.shell.timeout_seconds = 30; cfg->tools.shell.max_output_bytes = 65536; snprintf(cfg->tools.shell.working_directory, sizeof(cfg->tools.shell.working_directory), "%s", "."); cfg->security.verify_signatures = 1; cfg->security.admin.enabled = 1; cfg->security.admin.tools_enabled = 1; cfg->security.wot.enabled = 1; cfg->security.wot.tools_enabled = 0; cfg->security.stranger.enabled = 1; cfg->security.stranger.tools_enabled = 0; cfg->admin_context.enabled = 1; cfg->admin_context.track_kind_0 = 1; cfg->admin_context.track_kind_3 = 1; cfg->admin_context.track_kind_10002 = 1; cfg->admin_context.track_kind_1 = 1; cfg->admin_context.kind_1_limit = 10; cfg->triggers.enabled = 1; cfg->triggers.max_active = 16; cfg->triggers.cooldown_seconds = 60; cfg->triggers.llm_rate_limit_per_minute = 10; cfg->triggers.template_rate_limit_per_minute = 60; cfg->api.enabled = 1; cfg->api.port = 8484; snprintf(cfg->api.bind_address, sizeof(cfg->api.bind_address), "%s", "127.0.0.1"); snprintf(cfg->llm.provider, sizeof(cfg->llm.provider), "%s", "openai"); snprintf(cfg->llm.model, sizeof(cfg->llm.model), "%s", "gpt-4o-mini"); snprintf(cfg->llm.base_url, sizeof(cfg->llm.base_url), "%s", "https://api.openai.com/v1"); cfg->llm.max_tokens = 512; cfg->llm.temperature = 0.7; } static void free_relays_only(didactyl_config_t* cfg) { if (!cfg || !cfg->relays) return; for (int i = 0; i < cfg->relay_count; i++) { free(cfg->relays[i]); } free(cfg->relays); cfg->relays = NULL; cfg->relay_count = 0; } static int set_default_relays(didactyl_config_t* cfg) { if (!cfg) return -1; free_relays_only(cfg); cfg->relay_count = DIDACTYL_DEFAULT_RELAY_COUNT; cfg->relays = (char**)calloc((size_t)cfg->relay_count, sizeof(char*)); if (!cfg->relays) { cfg->relay_count = 0; return -1; } for (int i = 0; i < cfg->relay_count; i++) { cfg->relays[i] = strdup(DIDACTYL_DEFAULT_RELAYS[i]); if (!cfg->relays[i]) { free_relays_only(cfg); return -1; } } return 0; } static int build_kind10002_tags_json(const didactyl_config_t* cfg, char** out_tags_json) { if (!cfg || !out_tags_json || cfg->relay_count <= 0 || !cfg->relays) return -1; *out_tags_json = NULL; cJSON* tags = cJSON_CreateArray(); if (!tags) return -1; for (int i = 0; i < cfg->relay_count; i++) { if (!cfg->relays[i] || cfg->relays[i][0] == '\0') continue; cJSON* tag = cJSON_CreateArray(); if (!tag) { cJSON_Delete(tags); return -1; } cJSON_AddItemToArray(tag, cJSON_CreateString("r")); cJSON_AddItemToArray(tag, cJSON_CreateString(cfg->relays[i])); cJSON_AddItemToArray(tags, tag); } char* json = cJSON_PrintUnformatted(tags); cJSON_Delete(tags); if (!json) return -1; *out_tags_json = json; return 0; } static int upsert_startup_event(didactyl_config_t* cfg, int kind, const char* content, const char* tags_json) { if (!cfg || !content || !tags_json) { return -1; } for (int i = 0; i < cfg->startup_event_count; i++) { startup_event_t* se = &cfg->startup_events[i]; if (se->kind != kind) { continue; } char* content_dup = strdup(content); char* tags_dup = strdup(tags_json); if (!content_dup || !tags_dup) { free(content_dup); free(tags_dup); return -1; } free(se->content); free(se->tags_json); se->content = content_dup; se->tags_json = tags_dup; return 0; } startup_event_t* grown = (startup_event_t*)realloc(cfg->startup_events, (size_t)(cfg->startup_event_count + 1) * sizeof(startup_event_t)); if (!grown) { return -1; } cfg->startup_events = grown; startup_event_t* se = &cfg->startup_events[cfg->startup_event_count]; memset(se, 0, sizeof(*se)); se->kind = kind; se->content = strdup(content); se->tags_json = strdup(tags_json); if (!se->content || !se->tags_json) { free(se->content); free(se->tags_json); se->content = NULL; se->tags_json = NULL; return -1; } cfg->startup_event_count++; return 0; } static int apply_default_startup_events(didactyl_config_t* cfg, const char* agent_name) { if (!cfg || !agent_name || agent_name[0] == '\0' || cfg->admin.pubkey[0] == '\0') { return -1; } for (int i = 0; i < DIDACTYL_DEFAULT_STARTUP_EVENT_KIND_COUNT; i++) { int kind = DIDACTYL_DEFAULT_STARTUP_EVENT_KINDS[i]; if (kind == DIDACTYL_STARTUP_KIND_PROFILE) { int needed = snprintf(NULL, 0, DIDACTYL_DEFAULT_KIND0_PROFILE_JSON_TEMPLATE, agent_name, agent_name); if (needed <= 0) { return -1; } char* content_json = (char*)malloc((size_t)needed + 1U); if (!content_json) { return -1; } snprintf(content_json, (size_t)needed + 1U, DIDACTYL_DEFAULT_KIND0_PROFILE_JSON_TEMPLATE, agent_name, agent_name); int rc = upsert_startup_event(cfg, kind, content_json, "[]"); free(content_json); if (rc != 0) { return -1; } continue; } if (kind == DIDACTYL_STARTUP_KIND_CONTACTS) { int needed = snprintf(NULL, 0, DIDACTYL_DEFAULT_KIND3_TAGS_JSON_TEMPLATE, cfg->admin.pubkey); if (needed <= 0) { return -1; } char* tags_json = (char*)malloc((size_t)needed + 1U); if (!tags_json) { return -1; } snprintf(tags_json, (size_t)needed + 1U, DIDACTYL_DEFAULT_KIND3_TAGS_JSON_TEMPLATE, cfg->admin.pubkey); int rc = upsert_startup_event(cfg, kind, DIDACTYL_DEFAULT_KIND3_CONTENT, tags_json); free(tags_json); if (rc != 0) { return -1; } continue; } if (kind == DIDACTYL_STARTUP_KIND_RELAYS) { char* tags_json = NULL; if (build_kind10002_tags_json(cfg, &tags_json) != 0 || !tags_json) { free(tags_json); return -1; } int rc = upsert_startup_event(cfg, kind, DIDACTYL_DEFAULT_KIND10002_CONTENT, tags_json); free(tags_json); if (rc != 0) { return -1; } continue; } } return 0; } static int configure_default_skill_for_agent(didactyl_config_t* cfg, const char* agent_name) { if (!cfg || !agent_name || agent_name[0] == '\0') { return -1; } free(cfg->default_skill.content); cfg->default_skill.content = NULL; free(cfg->default_skill.tags_json); cfg->default_skill.tags_json = NULL; cfg->default_skill.kind = DIDACTYL_DEFAULT_SKILL_KIND; snprintf(cfg->default_skill.d_tag, sizeof(cfg->default_skill.d_tag), "%s", DIDACTYL_DEFAULT_SKILL_D_TAG); int needed = snprintf(NULL, 0, DIDACTYL_DEFAULT_SKILL_TEMPLATE, agent_name, agent_name); if (needed <= 0) { return -1; } cfg->default_skill.content = (char*)malloc((size_t)needed + 1U); if (!cfg->default_skill.content) { return -1; } snprintf(cfg->default_skill.content, (size_t)needed + 1U, DIDACTYL_DEFAULT_SKILL_TEMPLATE, agent_name, agent_name); cfg->default_skill.tags_json = strdup(DIDACTYL_DEFAULT_SKILL_TAGS_JSON); if (!cfg->default_skill.tags_json) { free(cfg->default_skill.content); cfg->default_skill.content = NULL; return -1; } return 0; } static int relay_contains(const didactyl_config_t* cfg, const char* relay) { if (!cfg || !relay || relay[0] == '\0') return 0; for (int i = 0; i < cfg->relay_count; i++) { if (cfg->relays[i] && strcmp(cfg->relays[i], relay) == 0) return 1; } return 0; } static int relay_add(didactyl_config_t* cfg, const char* relay) { if (!cfg || !relay || relay[0] == '\0') return -1; if (relay_contains(cfg, relay)) return 0; char** grown = (char**)realloc(cfg->relays, (size_t)(cfg->relay_count + 1) * sizeof(char*)); if (!grown) return -1; cfg->relays = grown; cfg->relays[cfg->relay_count] = strdup(relay); if (!cfg->relays[cfg->relay_count]) return -1; cfg->relay_count++; return 0; } static int relay_remove_index(didactyl_config_t* cfg, int idx) { if (!cfg || idx < 0 || idx >= cfg->relay_count || cfg->relay_count <= 1) return -1; free(cfg->relays[idx]); for (int i = idx; i < cfg->relay_count - 1; i++) { cfg->relays[i] = cfg->relays[i + 1]; } cfg->relay_count--; if (cfg->relay_count == 0) { free(cfg->relays); cfg->relays = NULL; return 0; } char** shrunk = (char**)realloc(cfg->relays, (size_t)cfg->relay_count * sizeof(char*)); if (shrunk) cfg->relays = shrunk; return 0; } static int wait_connected_relays_ms(int timeout_ms) { const int step = 100; int elapsed = 0; while (elapsed < timeout_ms) { if (nostr_handler_connected_relay_count() > 0) return 1; (void)nostr_handler_poll(step); elapsed += step; } return nostr_handler_connected_relay_count() > 0 ? 1 : 0; } static int query_self_kind10002_exists(const didactyl_config_t* cfg, int* out_exists) { if (!cfg || !out_exists || cfg->keys.public_key_hex[0] == '\0') return -1; *out_exists = 0; cJSON* filter = cJSON_CreateObject(); cJSON* kinds = cJSON_CreateArray(); cJSON* authors = cJSON_CreateArray(); if (!filter || !kinds || !authors) { cJSON_Delete(filter); cJSON_Delete(kinds); cJSON_Delete(authors); return -1; } cJSON_AddItemToArray(kinds, cJSON_CreateNumber(10002)); cJSON_AddItemToObject(filter, "kinds", kinds); cJSON_AddItemToArray(authors, cJSON_CreateString(cfg->keys.public_key_hex)); cJSON_AddItemToObject(filter, "authors", authors); cJSON_AddNumberToObject(filter, "limit", 1); char* events_json = nostr_handler_query_json(filter, 3000); cJSON_Delete(filter); if (events_json) { cJSON* arr = cJSON_Parse(events_json); if (arr && cJSON_IsArray(arr) && cJSON_GetArraySize(arr) > 0) { *out_exists = 1; } cJSON_Delete(arr); free(events_json); } return 0; } static int query_admin_kind0_name(const didactyl_config_t* cfg, char* out_name, size_t out_name_size, int* out_found) { if (!cfg || !out_name || out_name_size == 0 || !out_found || cfg->admin.pubkey[0] == '\0') { return -1; } out_name[0] = '\0'; *out_found = 0; cJSON* filter = cJSON_CreateObject(); cJSON* kinds = cJSON_CreateArray(); cJSON* authors = cJSON_CreateArray(); if (!filter || !kinds || !authors) { cJSON_Delete(filter); cJSON_Delete(kinds); cJSON_Delete(authors); return -1; } cJSON_AddItemToArray(kinds, cJSON_CreateNumber(0)); cJSON_AddItemToObject(filter, "kinds", kinds); cJSON_AddItemToArray(authors, cJSON_CreateString(cfg->admin.pubkey)); cJSON_AddItemToObject(filter, "authors", authors); cJSON_AddNumberToObject(filter, "limit", 1); char* events_json = nostr_handler_query_json(filter, 3000); cJSON_Delete(filter); if (!events_json) { return -1; } cJSON* arr = cJSON_Parse(events_json); free(events_json); if (!arr || !cJSON_IsArray(arr) || cJSON_GetArraySize(arr) <= 0) { cJSON_Delete(arr); return 0; } cJSON* ev = cJSON_GetArrayItem(arr, 0); cJSON* content = ev ? cJSON_GetObjectItemCaseSensitive(ev, "content") : NULL; if (!content || !cJSON_IsString(content) || !content->valuestring || content->valuestring[0] == '\0') { cJSON_Delete(arr); return 0; } cJSON* profile = cJSON_Parse(content->valuestring); if (!profile || !cJSON_IsObject(profile)) { cJSON_Delete(profile); cJSON_Delete(arr); return 0; } cJSON* display_name = cJSON_GetObjectItemCaseSensitive(profile, "display_name"); cJSON* name = cJSON_GetObjectItemCaseSensitive(profile, "name"); cJSON* chosen = NULL; if (display_name && cJSON_IsString(display_name) && display_name->valuestring && display_name->valuestring[0] != '\0') { chosen = display_name; } else if (name && cJSON_IsString(name) && name->valuestring && name->valuestring[0] != '\0') { chosen = name; } if (chosen && chosen->valuestring) { snprintf(out_name, out_name_size, "%s", chosen->valuestring); *out_found = 1; } cJSON_Delete(profile); cJSON_Delete(arr); return 0; } static int validate_new_agent_identity_and_admin(const didactyl_config_t* cfg, int* out_exists, char* out_admin_name, size_t out_admin_name_size, int* out_admin_name_found) { if (!cfg || !out_exists || !out_admin_name || out_admin_name_size == 0 || !out_admin_name_found) { return -1; } *out_exists = 0; *out_admin_name_found = 0; out_admin_name[0] = '\0'; didactyl_config_t tmp = *cfg; if (nostr_handler_init(&tmp) != 0) { return -1; } (void)wait_connected_relays_ms(4000); int rc_exists = query_self_kind10002_exists(cfg, out_exists); int rc_admin = query_admin_kind0_name(cfg, out_admin_name, out_admin_name_size, out_admin_name_found); nostr_handler_cleanup(); return (rc_exists == 0 && rc_admin == 0) ? 0 : -1; } static int query_and_extract_kind10002_relays(didactyl_config_t* cfg) { if (!cfg || cfg->keys.public_key_hex[0] == '\0') return -1; didactyl_config_t tmp = *cfg; if (nostr_handler_init(&tmp) != 0) { return -1; } (void)wait_connected_relays_ms(5000); cJSON* filter = cJSON_CreateObject(); cJSON* kinds = cJSON_CreateArray(); cJSON* authors = cJSON_CreateArray(); if (!filter || !kinds || !authors) { cJSON_Delete(filter); cJSON_Delete(kinds); cJSON_Delete(authors); nostr_handler_cleanup(); return -1; } cJSON_AddItemToArray(kinds, cJSON_CreateNumber(10002)); cJSON_AddItemToObject(filter, "kinds", kinds); cJSON_AddItemToArray(authors, cJSON_CreateString(cfg->keys.public_key_hex)); cJSON_AddItemToObject(filter, "authors", authors); cJSON_AddNumberToObject(filter, "limit", 8); char* events_json = nostr_handler_query_json(filter, 4000); cJSON_Delete(filter); if (!events_json) { nostr_handler_cleanup(); return -1; } cJSON* arr = cJSON_Parse(events_json); free(events_json); if (!arr || !cJSON_IsArray(arr) || cJSON_GetArraySize(arr) <= 0) { cJSON_Delete(arr); nostr_handler_cleanup(); return -1; } cJSON* ev = cJSON_GetArrayItem(arr, 0); cJSON* tags = ev ? cJSON_GetObjectItemCaseSensitive(ev, "tags") : NULL; if (!tags || !cJSON_IsArray(tags)) { cJSON_Delete(arr); nostr_handler_cleanup(); return -1; } didactyl_config_t new_relays = {0}; int n = cJSON_GetArraySize(tags); for (int i = 0; i < n; i++) { cJSON* tag = cJSON_GetArrayItem(tags, i); if (!tag || !cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) continue; cJSON* key = cJSON_GetArrayItem(tag, 0); cJSON* val = cJSON_GetArrayItem(tag, 1); if (!key || !val || !cJSON_IsString(key) || !cJSON_IsString(val) || !key->valuestring || !val->valuestring) continue; if (strcmp(key->valuestring, "r") == 0 && val->valuestring[0] != '\0') { (void)relay_add(&new_relays, val->valuestring); } } if (new_relays.relay_count > 0) { free_relays_only(cfg); cfg->relays = new_relays.relays; cfg->relay_count = new_relays.relay_count; } else { free_relays_only(&new_relays); } cJSON_Delete(arr); nostr_handler_cleanup(); return 0; } static int recover_existing_config_from_nostr(didactyl_config_t* cfg, int* out_relay_found) { if (!cfg) return -1; if (out_relay_found) *out_relay_found = 0; if (query_and_extract_kind10002_relays(cfg) == 0) { if (out_relay_found) *out_relay_found = cfg->relay_count > 0 ? 1 : 0; return 0; } return -1; } static int query_self_kind0_name(const didactyl_config_t* cfg, char* out_name, size_t out_name_size, int* out_found) { if (!cfg || !out_name || out_name_size == 0 || !out_found || cfg->keys.public_key_hex[0] == '\0') { return -1; } out_name[0] = '\0'; *out_found = 0; cJSON* filter = cJSON_CreateObject(); cJSON* kinds = cJSON_CreateArray(); cJSON* authors = cJSON_CreateArray(); if (!filter || !kinds || !authors) { cJSON_Delete(filter); cJSON_Delete(kinds); cJSON_Delete(authors); return -1; } cJSON_AddItemToArray(kinds, cJSON_CreateNumber(0)); cJSON_AddItemToObject(filter, "kinds", kinds); cJSON_AddItemToArray(authors, cJSON_CreateString(cfg->keys.public_key_hex)); cJSON_AddItemToObject(filter, "authors", authors); cJSON_AddNumberToObject(filter, "limit", 1); char* events_json = nostr_handler_query_json(filter, 3000); cJSON_Delete(filter); if (!events_json) { return -1; } cJSON* arr = cJSON_Parse(events_json); free(events_json); if (!arr || !cJSON_IsArray(arr) || cJSON_GetArraySize(arr) <= 0) { cJSON_Delete(arr); return 0; } cJSON* ev = cJSON_GetArrayItem(arr, 0); cJSON* content = ev ? cJSON_GetObjectItemCaseSensitive(ev, "content") : NULL; if (!content || !cJSON_IsString(content) || !content->valuestring || content->valuestring[0] == '\0') { cJSON_Delete(arr); return 0; } cJSON* profile = cJSON_Parse(content->valuestring); if (!profile || !cJSON_IsObject(profile)) { cJSON_Delete(profile); cJSON_Delete(arr); return 0; } cJSON* display_name = cJSON_GetObjectItemCaseSensitive(profile, "display_name"); cJSON* name = cJSON_GetObjectItemCaseSensitive(profile, "name"); cJSON* chosen = NULL; if (display_name && cJSON_IsString(display_name) && display_name->valuestring && display_name->valuestring[0] != '\0') { chosen = display_name; } else if (name && cJSON_IsString(name) && name->valuestring && name->valuestring[0] != '\0') { chosen = name; } if (chosen && chosen->valuestring) { snprintf(out_name, out_name_size, "%s", chosen->valuestring); *out_found = 1; } cJSON_Delete(profile); cJSON_Delete(arr); return 0; } static int fetch_and_decrypt_self_config_wizard(const didactyl_config_t* cfg, const char* d_tag, char** out_plaintext) { if (!cfg || !d_tag || !out_plaintext || cfg->keys.public_key_hex[0] == '\0') { return -1; } *out_plaintext = NULL; cJSON* filter = cJSON_CreateObject(); cJSON* kinds = cJSON_CreateArray(); cJSON* authors = cJSON_CreateArray(); cJSON* d_vals = cJSON_CreateArray(); if (!filter || !kinds || !authors || !d_vals) { cJSON_Delete(filter); cJSON_Delete(kinds); cJSON_Delete(authors); cJSON_Delete(d_vals); return -1; } cJSON_AddItemToArray(kinds, cJSON_CreateNumber(30078)); cJSON_AddItemToObject(filter, "kinds", kinds); cJSON_AddItemToArray(authors, cJSON_CreateString(cfg->keys.public_key_hex)); cJSON_AddItemToObject(filter, "authors", authors); cJSON_AddItemToArray(d_vals, cJSON_CreateString(d_tag)); cJSON_AddItemToObject(filter, "#d", d_vals); cJSON_AddNumberToObject(filter, "limit", 1); char* events_json = nostr_handler_query_json(filter, 4000); cJSON_Delete(filter); if (!events_json) { return -1; } cJSON* arr = cJSON_Parse(events_json); free(events_json); if (!arr || !cJSON_IsArray(arr) || cJSON_GetArraySize(arr) <= 0) { cJSON_Delete(arr); return -1; } cJSON* ev = cJSON_GetArrayItem(arr, 0); cJSON* content = ev ? cJSON_GetObjectItemCaseSensitive(ev, "content") : NULL; if (!content || !cJSON_IsString(content) || !content->valuestring || content->valuestring[0] == '\0') { cJSON_Delete(arr); return -1; } size_t out_cap = strlen(content->valuestring) + 1024U; char* plaintext = (char*)malloc(out_cap); if (!plaintext) { cJSON_Delete(arr); return -1; } int dec_rc = nostr_nip44_decrypt(cfg->keys.private_key, cfg->keys.public_key, content->valuestring, plaintext, out_cap); cJSON_Delete(arr); if (dec_rc != NOSTR_SUCCESS) { free(plaintext); return -1; } *out_plaintext = plaintext; return 0; } static int apply_recalled_llm_config_wizard(didactyl_config_t* cfg, const char* plaintext) { if (!cfg || !plaintext) return -1; cJSON* root = cJSON_Parse(plaintext); if (!root || !cJSON_IsObject(root)) { cJSON_Delete(root); return -1; } cJSON* provider = cJSON_GetObjectItemCaseSensitive(root, "provider"); cJSON* api_key = cJSON_GetObjectItemCaseSensitive(root, "api_key"); cJSON* model = cJSON_GetObjectItemCaseSensitive(root, "model"); cJSON* base_url = cJSON_GetObjectItemCaseSensitive(root, "base_url"); cJSON* max_tokens = cJSON_GetObjectItemCaseSensitive(root, "max_tokens"); cJSON* temperature = cJSON_GetObjectItemCaseSensitive(root, "temperature"); if (!api_key || !cJSON_IsString(api_key) || !api_key->valuestring || api_key->valuestring[0] == '\0' || !model || !cJSON_IsString(model) || !model->valuestring || model->valuestring[0] == '\0' || !base_url || !cJSON_IsString(base_url) || !base_url->valuestring || base_url->valuestring[0] == '\0') { cJSON_Delete(root); return -1; } if (provider && cJSON_IsString(provider) && provider->valuestring && provider->valuestring[0] != '\0') { snprintf(cfg->llm.provider, sizeof(cfg->llm.provider), "%s", provider->valuestring); } else if (cfg->llm.provider[0] == '\0') { snprintf(cfg->llm.provider, sizeof(cfg->llm.provider), "%s", "openai"); } snprintf(cfg->llm.api_key, sizeof(cfg->llm.api_key), "%s", api_key->valuestring); snprintf(cfg->llm.model, sizeof(cfg->llm.model), "%s", model->valuestring); snprintf(cfg->llm.base_url, sizeof(cfg->llm.base_url), "%s", base_url->valuestring); if (max_tokens && cJSON_IsNumber(max_tokens)) { cfg->llm.max_tokens = (int)max_tokens->valuedouble; } if (temperature && cJSON_IsNumber(temperature)) { cfg->llm.temperature = temperature->valuedouble; } cJSON_Delete(root); return 0; } static int apply_recalled_agent_config_wizard(didactyl_config_t* cfg, const char* plaintext) { if (!cfg || !plaintext) return -1; cJSON* root = cJSON_Parse(plaintext); if (!root || !cJSON_IsObject(root)) { cJSON_Delete(root); return -1; } cJSON* admin_pubkey = cJSON_GetObjectItemCaseSensitive(root, "admin_pubkey"); cJSON* dm_protocol = cJSON_GetObjectItemCaseSensitive(root, "dm_protocol"); if (admin_pubkey && cJSON_IsString(admin_pubkey) && admin_pubkey->valuestring && admin_pubkey->valuestring[0] != '\0') { char decoded[65] = {0}; if (decode_pubkey_hex_or_npub_local(admin_pubkey->valuestring, decoded) == 0) { snprintf(cfg->admin.pubkey, sizeof(cfg->admin.pubkey), "%s", decoded); } } if (dm_protocol && cJSON_IsString(dm_protocol) && dm_protocol->valuestring && dm_protocol->valuestring[0] != '\0') { if (strcmp(dm_protocol->valuestring, "nip17") == 0) { cfg->dm_protocol = DM_PROTOCOL_NIP17; } else if (strcmp(dm_protocol->valuestring, "both") == 0) { cfg->dm_protocol = DM_PROTOCOL_BOTH; } else { cfg->dm_protocol = DM_PROTOCOL_NIP04; } } cJSON_Delete(root); return 0; } static int recover_full_config_from_nostr(didactyl_config_t* cfg, char* out_agent_name, size_t out_agent_name_size, int* out_agent_name_found) { if (!cfg || !out_agent_name || out_agent_name_size == 0 || !out_agent_name_found) { return -1; } out_agent_name[0] = '\0'; *out_agent_name_found = 0; didactyl_config_t tmp = *cfg; if (nostr_handler_init(&tmp) != 0) { return -1; } (void)wait_connected_relays_ms(5000); (void)query_self_kind0_name(cfg, out_agent_name, out_agent_name_size, out_agent_name_found); char* llm_plaintext = NULL; if (fetch_and_decrypt_self_config_wizard(cfg, "llm_config", &llm_plaintext) == 0 && llm_plaintext) { (void)apply_recalled_llm_config_wizard(cfg, llm_plaintext); } free(llm_plaintext); char* agent_plaintext = NULL; if (fetch_and_decrypt_self_config_wizard(cfg, "agent_config", &agent_plaintext) == 0 && agent_plaintext) { (void)apply_recalled_agent_config_wizard(cfg, agent_plaintext); } free(agent_plaintext); nostr_handler_cleanup(); return 0; } static const char* dm_protocol_to_string_local(dm_protocol_t protocol) { switch (protocol) { case DM_PROTOCOL_NIP17: return "nip17"; case DM_PROTOCOL_BOTH: return "both"; case DM_PROTOCOL_NIP04: default: return "nip04"; } } static int publish_encrypted_self_config_wizard(const didactyl_config_t* cfg, const char* d_tag, const char* content_json) { if (!cfg || !d_tag || !content_json || d_tag[0] == '\0') return -1; size_t cipher_cap = (strlen(content_json) * 4U) + 1024U; char* ciphertext = (char*)malloc(cipher_cap); if (!ciphertext) return -1; int enc_rc = nostr_nip44_encrypt(cfg->keys.private_key, cfg->keys.public_key, content_json, ciphertext, cipher_cap); if (enc_rc != NOSTR_SUCCESS) { free(ciphertext); return -1; } cJSON* tags = cJSON_CreateArray(); cJSON* d = cJSON_CreateArray(); cJSON* app = cJSON_CreateArray(); if (!tags || !d || !app) { cJSON_Delete(tags); cJSON_Delete(d); cJSON_Delete(app); free(ciphertext); return -1; } cJSON_AddItemToArray(d, cJSON_CreateString("d")); cJSON_AddItemToArray(d, cJSON_CreateString(d_tag)); cJSON_AddItemToArray(tags, d); cJSON_AddItemToArray(app, cJSON_CreateString("app")); cJSON_AddItemToArray(app, cJSON_CreateString("didactyl")); cJSON_AddItemToArray(tags, app); nostr_publish_result_t result; memset(&result, 0, sizeof(result)); int rc = nostr_handler_publish_kind_event(30078, ciphertext, tags, &result); nostr_handler_publish_result_free(&result); cJSON_Delete(tags); free(ciphertext); return rc; } static int persist_runtime_config_to_nostr_wizard(const didactyl_config_t* cfg) { if (!cfg) return -1; cJSON* llm = cJSON_CreateObject(); if (!llm) return -1; cJSON_AddStringToObject(llm, "provider", cfg->llm.provider); cJSON_AddStringToObject(llm, "api_key", cfg->llm.api_key); cJSON_AddStringToObject(llm, "model", cfg->llm.model); cJSON_AddStringToObject(llm, "base_url", cfg->llm.base_url); cJSON_AddNumberToObject(llm, "max_tokens", cfg->llm.max_tokens); cJSON_AddNumberToObject(llm, "temperature", cfg->llm.temperature); char* llm_json = cJSON_PrintUnformatted(llm); cJSON_Delete(llm); if (!llm_json) return -1; int llm_rc = publish_encrypted_self_config_wizard(cfg, "llm_config", llm_json); free(llm_json); cJSON* agent = cJSON_CreateObject(); if (!agent) return llm_rc; cJSON_AddStringToObject(agent, "admin_pubkey", cfg->admin.pubkey); cJSON_AddStringToObject(agent, "dm_protocol", dm_protocol_to_string_local(cfg->dm_protocol)); char* agent_json = cJSON_PrintUnformatted(agent); cJSON_Delete(agent); if (!agent_json) return llm_rc; int agent_rc = publish_encrypted_self_config_wizard(cfg, "agent_config", agent_json); free(agent_json); return (llm_rc == 0 && agent_rc == 0) ? 0 : -1; } static int persist_runtime_config_to_nostr_wizard_online(const didactyl_config_t* cfg) { if (!cfg) return -1; didactyl_config_t tmp = *cfg; if (nostr_handler_init(&tmp) != 0) { return -1; } (void)wait_connected_relays_ms(5000); int rc = persist_runtime_config_to_nostr_wizard(cfg); /* Give relays time to acknowledge the published events before tearing down the connection. Without this, the fire-and-forget publish may not reach any relay before cleanup closes the sockets. */ for (int i = 0; i < 30; i++) { nostr_handler_poll(100); } nostr_handler_cleanup(); return rc; } static int publish_kind10002_relays_wizard_online(const didactyl_config_t* cfg) { if (!cfg) return -1; didactyl_config_t tmp = *cfg; if (nostr_handler_init(&tmp) != 0) { return -1; } (void)wait_connected_relays_ms(5000); char* tags_json = NULL; if (build_kind10002_tags_json(cfg, &tags_json) != 0 || !tags_json) { free(tags_json); nostr_handler_cleanup(); return -1; } cJSON* tags = cJSON_Parse(tags_json); free(tags_json); if (!tags || !cJSON_IsArray(tags)) { cJSON_Delete(tags); nostr_handler_cleanup(); return -1; } nostr_publish_result_t result; memset(&result, 0, sizeof(result)); int rc = nostr_handler_publish_kind_event(10002, "", tags, &result); nostr_handler_publish_result_free(&result); cJSON_Delete(tags); nostr_handler_cleanup(); return rc; } static int ensure_startup_kind10002_from_current_relays(didactyl_config_t* cfg) { if (!cfg) return -1; char* tags_json = NULL; if (build_kind10002_tags_json(cfg, &tags_json) != 0 || !tags_json) { free(tags_json); return -1; } int rc = upsert_startup_event(cfg, 10002, "", tags_json); free(tags_json); return rc; } static int wizard_llm_test(const llm_config_t* llm_cfg) { if (!llm_cfg) return -1; if (llm_init(llm_cfg) != 0) { return -1; } char* response = llm_chat("You are a setup validator.", "Reply with exactly: OK"); llm_cleanup(); if (!response) { return -1; } free(response); return 0; } static int strcasestr_like(const char* haystack, const char* needle) { if (!haystack || !needle) return 0; if (needle[0] == '\0') return 1; size_t hlen = strlen(haystack); size_t nlen = strlen(needle); if (nlen > hlen) return 0; for (size_t i = 0; i + nlen <= hlen; i++) { size_t j = 0; while (j < nlen) { unsigned char hc = (unsigned char)tolower((unsigned char)haystack[i + j]); unsigned char nc = (unsigned char)tolower((unsigned char)needle[j]); if (hc != nc) break; j++; } if (j == nlen) return 1; } return 0; } static void append_model_id_local(cJSON* ids, cJSON* item) { if (!ids || !item) return; if (cJSON_IsString(item) && item->valuestring) { cJSON_AddItemToArray(ids, cJSON_CreateString(item->valuestring)); return; } if (cJSON_IsObject(item)) { cJSON* id = cJSON_GetObjectItemCaseSensitive(item, "id"); if (id && cJSON_IsString(id) && id->valuestring) { cJSON_AddItemToArray(ids, cJSON_CreateString(id->valuestring)); } } } static int fetch_models_for_llm_config(const llm_config_t* llm_cfg, cJSON** out_models) { if (!llm_cfg || !out_models) return -1; *out_models = NULL; if (llm_init(llm_cfg) != 0) { return -1; } char* raw = llm_list_models_json(NULL); llm_cleanup(); if (!raw) { return -1; } cJSON* root = cJSON_Parse(raw); free(raw); if (!root) { cJSON_Delete(root); return -1; } cJSON* ids = cJSON_CreateArray(); if (!ids) { cJSON_Delete(root); return -1; } if (cJSON_IsArray(root)) { int n = cJSON_GetArraySize(root); for (int i = 0; i < n; i++) { append_model_id_local(ids, cJSON_GetArrayItem(root, i)); } } else if (cJSON_IsObject(root)) { cJSON* data = cJSON_GetObjectItemCaseSensitive(root, "data"); if (data && cJSON_IsArray(data)) { int n = cJSON_GetArraySize(data); for (int i = 0; i < n; i++) { append_model_id_local(ids, cJSON_GetArrayItem(data, i)); } } } cJSON_Delete(root); *out_models = ids; return 0; } static int extract_balance_value(const cJSON* root, double* out_balance) { if (!root || !out_balance || !cJSON_IsObject(root)) return -1; const char* keys[] = { "balance", "credits", "credit", "remaining", "remaining_credits", "available_balance" }; for (size_t i = 0; i < sizeof(keys) / sizeof(keys[0]); i++) { cJSON* v = cJSON_GetObjectItemCaseSensitive((cJSON*)root, keys[i]); if (v && cJSON_IsNumber(v)) { *out_balance = v->valuedouble; return 0; } } cJSON* data = cJSON_GetObjectItemCaseSensitive((cJSON*)root, "data"); if (data && cJSON_IsObject(data)) { for (size_t i = 0; i < sizeof(keys) / sizeof(keys[0]); i++) { cJSON* v = cJSON_GetObjectItemCaseSensitive(data, keys[i]); if (v && cJSON_IsNumber(v)) { *out_balance = v->valuedouble; return 0; } } } return -1; } static void wizard_check_provider_balance(const llm_config_t* llm_cfg) { if (!llm_cfg || llm_cfg->base_url[0] == '\0') return; const char* paths_ppq[] = {"/credits", "/credit", "/balance", "/v1/credits"}; const char* paths_routstr[] = {"/balance", "/credits", "/v1/balance"}; const char** paths = NULL; size_t path_count = 0; if (strcmp(llm_cfg->provider, "ppq") == 0) { paths = paths_ppq; path_count = sizeof(paths_ppq) / sizeof(paths_ppq[0]); } else if (strcmp(llm_cfg->provider, "routstr") == 0) { paths = paths_routstr; path_count = sizeof(paths_routstr) / sizeof(paths_routstr[0]); } else { fprintf(stderr, " Balance check: skipped for provider '%s'\n", llm_cfg->provider); return; } if (llm_init(llm_cfg) != 0) { fprintf(stderr, " Balance check: unavailable (LLM init failed)\n"); return; } for (size_t i = 0; i < path_count; i++) { char* raw = llm_get_json_path(NULL, paths[i]); if (!raw) continue; cJSON* root = cJSON_Parse(raw); free(raw); if (!root) continue; double balance = 0.0; if (extract_balance_value(root, &balance) == 0) { fprintf(stderr, " Balance check: available balance %.4f\n", balance); cJSON_Delete(root); llm_cleanup(); return; } cJSON_Delete(root); } llm_cleanup(); fprintf(stderr, " Balance check: endpoint unavailable or unrecognized response (continuing)\n"); } static int prompt_model_selection(const cJSON* models, const char* default_model, char* out_model, size_t out_model_size) { if (!out_model || out_model_size == 0 || !default_model || default_model[0] == '\0') return -1; char filter[WIZARD_LINE_MAX] = {0}; for (;;) { int total = (models && cJSON_IsArray(models)) ? cJSON_GetArraySize((cJSON*)models) : 0; int matched_count = 0; int matched_idx[256] = {0}; for (int i = 0; i < total && matched_count < (int)(sizeof(matched_idx) / sizeof(matched_idx[0])); i++) { cJSON* it = cJSON_GetArrayItem((cJSON*)models, i); if (!it || !cJSON_IsString(it) || !it->valuestring) continue; if (strcasestr_like(it->valuestring, filter)) { matched_idx[matched_count++] = i; } } fprintf(stderr, "\n Model Search\n"); fprintf(stderr, " Filter [%s] Matches: %d/%d\n", filter[0] ? filter : "(none)", matched_count, total); int show = matched_count < 20 ? matched_count : 20; for (int i = 0; i < show; i++) { cJSON* it = cJSON_GetArrayItem((cJSON*)models, matched_idx[i]); fprintf(stderr, " %d. %s\n", i + 1, it->valuestring); } if (matched_count > show) { fprintf(stderr, " ... (%d more matches)\n", matched_count - show); } fprintf(stderr, "\n Enter a number to choose, '/text' to set filter, exact model id for manual entry,\n" " or press Enter for default [%s]: ", default_model); char input[WIZARD_LINE_MAX] = {0}; if (read_line_prompt(NULL, input, sizeof(input)) != 0) return -1; if (line_is_quit(input)) return -1; if (input[0] == '\0') { snprintf(out_model, out_model_size, "%s", default_model); return 0; } if (input[0] == '/') { snprintf(filter, sizeof(filter), "%s", input + 1); continue; } int n = atoi(input); if (n > 0 && n <= show) { cJSON* it = cJSON_GetArrayItem((cJSON*)models, matched_idx[n - 1]); if (it && cJSON_IsString(it) && it->valuestring) { snprintf(out_model, out_model_size, "%s", it->valuestring); return 0; } } snprintf(out_model, out_model_size, "%s", input); return 0; } } static int prompt_admin_pubkey_with_header(didactyl_config_t* cfg, const char* step_title, const char* section_title) { char input[WIZARD_LINE_MAX]; for (;;) { render_wizard_page_header(step_title, section_title); if (read_line_prompt(" Enter the admin's Nostr public key (npub1... or hex):\n> ", input, sizeof(input)) != 0) { return -1; } if (line_is_quit(input)) return -1; char hex[65] = {0}; if (decode_pubkey_hex_or_npub_local(input, hex) != 0) { fprintf(stderr, "%sInvalid admin pubkey. Use npub1... or 64-char hex.%s\n", ANSI_RED, ANSI_RESET); continue; } snprintf(cfg->admin.pubkey, sizeof(cfg->admin.pubkey), "%s", hex); return 0; } } static int prompt_admin_pubkey(didactyl_config_t* cfg) { return prompt_admin_pubkey_with_header(cfg, "Step 3 of 7", "New Agent Setup -- Administrator"); } static int prompt_llm_config_with_header(didactyl_config_t* cfg, const char* step_title, const char* section_title) { for (;;) { render_wizard_page_header(step_title, section_title); print_option('1', " ppq.ai"); print_option('2', " Routstr"); print_option('3', " OpenAI-compatible"); print_option('q', "uit"); wizard_option_t provider_opts[] = {{'1', ""}, {'2', ""}, {'3', ""}, {'q', ""}}; char provider_choice = read_menu_choice(provider_opts, 4); if (provider_choice == 'q' || provider_choice == '\0') return -1; char provider[32] = {0}; char base_url[OW_MAX_URL_LEN] = {0}; if (provider_choice == '1') { snprintf(provider, sizeof(provider), "%s", "ppq"); snprintf(base_url, sizeof(base_url), "%s", "https://api.ppq.ai"); } else if (provider_choice == '2') { snprintf(provider, sizeof(provider), "%s", "routstr"); snprintf(base_url, sizeof(base_url), "%s", "https://api.routstr.com"); } else { snprintf(provider, sizeof(provider), "%s", "openai"); const char* default_base_url = cfg->llm.base_url[0] ? cfg->llm.base_url : "https://api.openai.com/v1"; char prompt[256]; snprintf(prompt, sizeof(prompt), " Base URL [%.200s]: ", default_base_url); if (read_line_prompt(prompt, base_url, sizeof(base_url)) != 0) return -1; if (line_is_quit(base_url)) return -1; if (base_url[0] == '\0') { snprintf(base_url, sizeof(base_url), "%s", default_base_url); } } char api_key[OW_MAX_KEY_LEN] = {0}; if (read_secret_prompt(" API Key: ", api_key, sizeof(api_key)) != 0) return -1; if (line_is_quit(api_key)) return -1; llm_config_t probe = cfg->llm; snprintf(probe.provider, sizeof(probe.provider), "%s", provider); snprintf(probe.base_url, sizeof(probe.base_url), "%s", base_url); snprintf(probe.api_key, sizeof(probe.api_key), "%s", api_key); if (probe.model[0] == '\0') { snprintf(probe.model, sizeof(probe.model), "%s", "claude-haiku-4.5"); } wizard_check_provider_balance(&probe); cJSON* models = NULL; if (fetch_models_for_llm_config(&probe, &models) != 0 || !models || !cJSON_IsArray(models)) { fprintf(stderr, "%sModel discovery failed (GET /models).%s\n", ANSI_RED, ANSI_RESET); print_option('r', "e-enter provider settings"); print_option('m', "anually enter model and continue"); print_option('q', "uit"); wizard_option_t fail_opts[] = {{'r', ""}, {'m', ""}, {'q', ""}}; char c = read_menu_choice(fail_opts, 3); if (c == 'q' || c == '\0') { cJSON_Delete(models); return -1; } if (c == 'r') { cJSON_Delete(models); continue; } char model[OW_MAX_MODEL_LEN] = {0}; if (read_line_prompt(" Model [claude-haiku-4.5]: ", model, sizeof(model)) != 0) { cJSON_Delete(models); return -1; } if (line_is_quit(model)) { cJSON_Delete(models); return -1; } if (model[0] == '\0') { snprintf(model, sizeof(model), "%s", "claude-haiku-4.5"); } snprintf(cfg->llm.provider, sizeof(cfg->llm.provider), "%s", provider); snprintf(cfg->llm.base_url, sizeof(cfg->llm.base_url), "%s", base_url); snprintf(cfg->llm.api_key, sizeof(cfg->llm.api_key), "%s", api_key); snprintf(cfg->llm.model, sizeof(cfg->llm.model), "%s", model); cfg->llm.max_tokens = cfg->llm.max_tokens > 0 ? cfg->llm.max_tokens : 512; cfg->llm.temperature = cfg->llm.temperature > 0.0 ? cfg->llm.temperature : 0.7; cJSON_Delete(models); return 0; } char model[OW_MAX_MODEL_LEN] = {0}; if (prompt_model_selection(models, "claude-haiku-4.5", model, sizeof(model)) != 0) { cJSON_Delete(models); return -1; } cJSON_Delete(models); int max_tokens = cfg->llm.max_tokens > 0 ? cfg->llm.max_tokens : 512; double temperature = cfg->llm.temperature > 0.0 ? cfg->llm.temperature : 0.7; snprintf(cfg->llm.provider, sizeof(cfg->llm.provider), "%s", provider); snprintf(cfg->llm.base_url, sizeof(cfg->llm.base_url), "%s", base_url); snprintf(cfg->llm.api_key, sizeof(cfg->llm.api_key), "%s", api_key); snprintf(cfg->llm.model, sizeof(cfg->llm.model), "%s", model); cfg->llm.max_tokens = max_tokens; cfg->llm.temperature = temperature; if (wizard_llm_test(&cfg->llm) == 0) { fprintf(stderr, "%sLLM test: OK%s\n", ANSI_YELLOW, ANSI_RESET); return 0; } fprintf(stderr, "%sLLM test: FAILED%s\n\n", ANSI_RED, ANSI_RESET); wizard_option_t opts[] = { {'r', "e-enter LLM settings"}, {'s', "kip test and continue anyway"}, {'q', "uit"} }; print_option('r', "e-enter LLM settings"); print_option('s', "kip test and continue anyway"); print_option('q', "uit"); char c = read_menu_choice(opts, 3); if (c == 's') return 0; if (c == 'q' || c == '\0') return -1; } } static int prompt_llm_config(didactyl_config_t* cfg) { return prompt_llm_config_with_header(cfg, "Step 5 of 7", "New Agent Setup -- LLM Provider"); } static int prompt_relay_configuration_with_header(didactyl_config_t* cfg, const char* step_title, const char* section_title) { char input[WIZARD_LINE_MAX]; for (;;) { render_wizard_page_header(step_title, section_title); fprintf(stderr, " Current relays:\n"); for (int i = 0; i < cfg->relay_count; i++) { fprintf(stderr, " %d. %s\n", i + 1, cfg->relays[i] ? cfg->relays[i] : ""); } fprintf(stderr, "\n"); print_option('a', "dd a relay"); print_option('r', "emove a relay (by number)"); print_option('d', "one -- use this list"); print_option('b', "ack"); print_option('q', "uit"); wizard_option_t opts[] = { {'a', "dd"}, {'r', "emove"}, {'d', "one"}, {'b', "ack"}, {'q', "uit"} }; char c = read_menu_choice(opts, 5); if (c == 'q') return -1; if (c == 'b') return 1; if (c == 'd') { if (cfg->relay_count <= 0) { fprintf(stderr, "%sAt least one relay is required.%s\n", ANSI_RED, ANSI_RESET); continue; } return 0; } if (c == 'a') { if (read_line_prompt("Relay URL (wss:// or ws://): ", input, sizeof(input)) != 0) return -1; if (line_is_quit(input)) return -1; if (!(strncmp(input, "wss://", 6) == 0 || strncmp(input, "ws://", 5) == 0)) { fprintf(stderr, "%sRelay must start with wss:// or ws://%s\n", ANSI_RED, ANSI_RESET); continue; } if (relay_add(cfg, input) != 0) { fprintf(stderr, "%sFailed to add relay.%s\n", ANSI_RED, ANSI_RESET); } continue; } if (c == 'r') { if (cfg->relay_count <= 1) { fprintf(stderr, "%sCannot remove last relay.%s\n", ANSI_RED, ANSI_RESET); continue; } if (read_line_prompt("Remove which number? ", input, sizeof(input)) != 0) return -1; if (line_is_quit(input)) return -1; int idx = atoi(input) - 1; if (relay_remove_index(cfg, idx) != 0) { fprintf(stderr, "%sInvalid relay number.%s\n", ANSI_RED, ANSI_RESET); } } } } static int prompt_relay_configuration(didactyl_config_t* cfg) { return prompt_relay_configuration_with_header(cfg, "Step 6 of 7", "New Agent Setup -- Relay Configuration"); } static int run_command_local(char* const argv[]) { pid_t pid = fork(); if (pid < 0) return -1; if (pid == 0) { execvp(argv[0], argv); _exit(127); } int status = 0; if (waitpid(pid, &status, 0) < 0) return -1; if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) return -1; return 0; } static int run_privileged_command_local(char* const argv[]) { if (!argv || !argv[0]) return -1; if (geteuid() == 0) { return run_command_local(argv); } char* sudo_argv[64] = {0}; sudo_argv[0] = "sudo"; int i = 0; for (; argv[i] && i < 62; i++) { sudo_argv[i + 1] = argv[i]; } if (argv[i] != NULL) { return -1; } sudo_argv[i + 1] = NULL; return run_command_local(sudo_argv); } static int write_file_privileged_local(const char* path, const char* content, unsigned int mode) { if (!path || path[0] == '\0' || !content) return -1; char tmp_template[] = "/tmp/didactyl-wizard-XXXXXX"; int fd = mkstemp(tmp_template); if (fd < 0) { return -1; } FILE* fp = fdopen(fd, "w"); if (!fp) { close(fd); unlink(tmp_template); return -1; } size_t len = strlen(content); int ok = (fwrite(content, 1, len, fp) == len) ? 1 : 0; if (fclose(fp) != 0) { ok = 0; } if (!ok) { unlink(tmp_template); return -1; } char mode_buf[16] = {0}; snprintf(mode_buf, sizeof(mode_buf), "%04o", mode & 07777U); char* install_argv[] = {"install", "-m", mode_buf, tmp_template, (char*)path, NULL}; int rc = run_privileged_command_local(install_argv); unlink(tmp_template); return rc; } static void sanitize_service_user_from_name(const char* name, char* out, size_t out_size) { if (!out || out_size == 0) return; out[0] = '\0'; const char* src = (name && name[0] != '\0') ? name : "didactyl"; size_t j = 0; for (size_t i = 0; src[i] != '\0' && j + 1 < out_size; i++) { char c = (char)tolower((unsigned char)src[i]); if ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9')) { out[j++] = c; } else if ((c == ' ' || c == '_' || c == '-') && j > 0 && out[j - 1] != '-') { out[j++] = '-'; } if (j >= 31) break; } while (j > 0 && out[j - 1] == '-') j--; out[j] = '\0'; if (out[0] == '\0' || !(out[0] >= 'a' && out[0] <= 'z')) { snprintf(out, out_size, "didactyl"); } } static const char* detect_ca_bundle_path_local(void) { const char* env = getenv("SSL_CERT_FILE"); if (env && env[0] != '\0' && access(env, R_OK) == 0) { return env; } static const char* candidates[] = { "/etc/ssl/certs/ca-certificates.crt", "/etc/ssl/cert.pem", "/etc/pki/tls/certs/ca-bundle.crt", "/etc/ssl/ca-bundle.pem" }; for (size_t i = 0; i < sizeof(candidates) / sizeof(candidates[0]); i++) { if (access(candidates[i], R_OK) == 0) { return candidates[i]; } } return NULL; } static int write_sudoers_file(const char* service_user) { if (!service_user || service_user[0] == '\0') return -1; char sudoers_path[PATH_MAX] = {0}; snprintf(sudoers_path, sizeof(sudoers_path), "/etc/sudoers.d/%s", service_user); char content[4096] = {0}; int n = snprintf(content, sizeof(content), "# Didactyl agent — scoped system maintenance privileges\n" "# Generated by didactyl setup wizard\n" "\n" "# Service management\n" "%s ALL=(ALL) NOPASSWD: /usr/bin/systemctl status *\n" "%s ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart *\n" "%s ALL=(ALL) NOPASSWD: /usr/bin/systemctl start *\n" "%s ALL=(ALL) NOPASSWD: /usr/bin/systemctl stop *\n" "%s ALL=(ALL) NOPASSWD: /usr/bin/systemctl enable *\n" "%s ALL=(ALL) NOPASSWD: /usr/bin/systemctl disable *\n" "%s ALL=(ALL) NOPASSWD: /usr/bin/systemctl is-active *\n" "%s ALL=(ALL) NOPASSWD: /usr/bin/systemctl is-enabled *\n" "%s ALL=(ALL) NOPASSWD: /usr/bin/systemctl list-units *\n" "%s ALL=(ALL) NOPASSWD: /usr/bin/systemctl daemon-reload\n" "\n" "# Log inspection\n" "%s ALL=(ALL) NOPASSWD: /usr/bin/journalctl *\n", service_user, service_user, service_user, service_user, service_user, service_user, service_user, service_user, service_user, service_user, service_user); if (n < 0 || (size_t)n >= sizeof(content)) { fprintf(stderr, "%sFailed to build sudoers content for %s.%s\n", ANSI_RED, service_user, ANSI_RESET); return -1; } if (write_file_privileged_local(sudoers_path, content, 0440U) != 0) { fprintf(stderr, "%sFailed to write sudoers file at %s.%s\n", ANSI_RED, sudoers_path, ANSI_RESET); return -1; } return 0; } static int install_system_service_with_dedicated_user(const didactyl_config_t* cfg, const char* agent_name) { if (!cfg || cfg->keys.nsec[0] == '\0') return -1; if (geteuid() != 0) { fprintf(stderr, " Requesting sudo authentication for system install...\n"); char* sudo_validate_argv[] = {"sudo", "-v", NULL}; if (run_command_local(sudo_validate_argv) != 0) { fprintf(stderr, "%sSudo authentication failed. System install canceled.%s\n", ANSI_RED, ANSI_RESET); return -1; } } /* --- Derive service username from agent name --- */ char service_user[64] = {0}; sanitize_service_user_from_name(agent_name, service_user, sizeof(service_user)); /* --- Create dedicated user if it doesn't exist --- */ struct passwd* pw = getpwnam(service_user); if (!pw) { fprintf(stderr, " Creating system user '%s'...\n", service_user); char comment[128] = {0}; snprintf(comment, sizeof(comment), "Didactyl Nostr Agent (%s)", agent_name); char* useradd_argv[] = {"useradd", "-m", "-s", "/bin/bash", "-c", comment, service_user, NULL}; if (run_privileged_command_local(useradd_argv) != 0) { fprintf(stderr, "%sFailed to create system user '%s'.%s\n", ANSI_RED, service_user, ANSI_RESET); return -1; } pw = getpwnam(service_user); if (!pw) { fprintf(stderr, "%sCreated user but failed to resolve passwd entry.%s\n", ANSI_RED, ANSI_RESET); return -1; } } else { fprintf(stderr, " User '%s' already exists, reusing.\n", service_user); } const char* home = (pw->pw_dir && pw->pw_dir[0] != '\0') ? pw->pw_dir : NULL; if (!home) { fprintf(stderr, "%sUnable to determine service user home directory.%s\n", ANSI_RED, ANSI_RESET); return -1; } /* --- Copy binary --- */ char self_path[PATH_MAX] = {0}; ssize_t self_len = readlink("/proc/self/exe", self_path, sizeof(self_path) - 1); if (self_len <= 0) { fprintf(stderr, "%sUnable to resolve running binary path.%s\n", ANSI_RED, ANSI_RESET); return -1; } self_path[self_len] = '\0'; char bin_path[PATH_MAX] = {0}; snprintf(bin_path, sizeof(bin_path), "%s/didactyl", home); fprintf(stderr, " Copying binary to %s...\n", bin_path); char* install_bin_argv[] = {"install", "-m", "0755", self_path, bin_path, NULL}; if (run_privileged_command_local(install_bin_argv) != 0) { fprintf(stderr, "%sFailed to copy binary to %s.%s\n", ANSI_RED, bin_path, ANSI_RESET); return -1; } /* --- Set ownership on home directory and installed binary --- */ char owner_spec[64] = {0}; snprintf(owner_spec, sizeof(owner_spec), "%lu:%lu", (unsigned long)pw->pw_uid, (unsigned long)pw->pw_gid); char* chown_home_argv[] = {"chown", owner_spec, (char*)home, NULL}; char* chmod_home_argv[] = {"chmod", "0750", (char*)home, NULL}; char* chown_bin_argv[] = {"chown", owner_spec, bin_path, NULL}; if (run_privileged_command_local(chown_home_argv) != 0 || run_privileged_command_local(chmod_home_argv) != 0 || run_privileged_command_local(chown_bin_argv) != 0) { fprintf(stderr, "%sFailed to set ownership/permissions for installed files.%s\n", ANSI_RED, ANSI_RESET); return -1; } /* --- Write sudoers file for scoped sudo privileges --- */ fprintf(stderr, " Writing sudoers file for '%s'...\n", service_user); if (write_sudoers_file(service_user) != 0) { fprintf(stderr, "%sWarning: Failed to write sudoers file. Agent will not have sudo access.%s\n", ANSI_YELLOW, ANSI_RESET); /* Non-fatal — agent can still run without sudo */ } /* --- Detect CA bundle for TLS --- */ const char* ca_bundle = detect_ca_bundle_path_local(); /* --- Write systemd unit file --- */ char service_name[96] = {0}; snprintf(service_name, sizeof(service_name), "%s.service", service_user); char service_path[PATH_MAX] = {0}; snprintf(service_path, sizeof(service_path), "/etc/systemd/system/%s", service_name); fprintf(stderr, " Writing systemd unit to %s...\n", service_path); char env_line[PATH_MAX + 64] = {0}; if (ca_bundle) { snprintf(env_line, sizeof(env_line), "Environment=SSL_CERT_FILE=%s\\n", ca_bundle); } char service_content[8192] = {0}; int svc_len = snprintf(service_content, sizeof(service_content), "[Unit]\n" "Description=Didactyl Nostr Agent (%s)\n" "After=network-online.target\n" "Wants=network-online.target\n\n" "[Service]\n" "Type=simple\n" "User=%s\n" "Group=%s\n" "WorkingDirectory=%s\n" "ExecStart=%s --nsec %s --debug 3\n" "%s" "SyslogIdentifier=%s\n" "NoNewPrivileges=false\n" "ProtectSystem=strict\n" "ReadWritePaths=%s\n" "ProtectHome=false\n" "PrivateTmp=yes\n" "Restart=on-failure\n" "RestartSec=10\n" "StandardOutput=journal\n" "StandardError=journal\n\n" "[Install]\n" "WantedBy=multi-user.target\n", service_user, service_user, service_user, home, bin_path, cfg->keys.nsec, env_line, service_user, home); if (svc_len < 0 || (size_t)svc_len >= sizeof(service_content)) { fprintf(stderr, "%sFailed to build service file content.%s\n", ANSI_RED, ANSI_RESET); return -1; } if (write_file_privileged_local(service_path, service_content, 0644U) != 0) { fprintf(stderr, "%sFailed to write service file at %s.%s\n", ANSI_RED, service_path, ANSI_RESET); return -1; } /* --- Enable and start the service --- */ fprintf(stderr, " Enabling and starting %s...\n", service_name); char* reload_argv[] = {"systemctl", "daemon-reload", NULL}; char* enable_argv[] = {"systemctl", "enable", service_name, NULL}; char* start_argv[] = {"systemctl", "start", service_name, NULL}; if (run_privileged_command_local(reload_argv) != 0 || run_privileged_command_local(enable_argv) != 0 || run_privileged_command_local(start_argv) != 0) { fprintf(stderr, "%sFailed to enable/start systemd service %s.%s\n", ANSI_RED, service_name, ANSI_RESET); fprintf(stderr, " You can manually start with:\n"); fprintf(stderr, " sudo systemctl daemon-reload\n"); fprintf(stderr, " sudo systemctl enable %s\n", service_name); fprintf(stderr, " sudo systemctl start %s\n", service_name); return -1; } fprintf(stderr, "\n%sSystem service installed and started.%s\n", ANSI_YELLOW, ANSI_RESET); fprintf(stderr, " Service: %s\n", service_name); fprintf(stderr, " User: %s\n", service_user); fprintf(stderr, " Home: %s\n", home); fprintf(stderr, " Binary: %s\n", bin_path); fprintf(stderr, " Nsec: embedded directly in ExecStart\n"); fprintf(stderr, " Sudoers: /etc/sudoers.d/%s\n", service_user); fprintf(stderr, " Unit: %s\n", service_path); fprintf(stderr, "\n Useful commands:\n"); fprintf(stderr, " sudo systemctl status %s\n", service_name); fprintf(stderr, " sudo journalctl -u %s -f\n", service_name); fprintf(stderr, " sudo systemctl restart %s\n", service_name); return 0; } static __attribute__((unused)) int maybe_prompt_write_path(char* path_out, size_t path_out_size) { if (!path_out || path_out_size == 0) return -1; char input[WIZARD_LINE_MAX]; if (read_line_prompt("Path [./genesis.jsonc]: ", input, sizeof(input)) != 0) return -1; if (line_is_quit(input)) return -1; if (input[0] == '\0') { snprintf(path_out, path_out_size, "%s", "./genesis.jsonc"); } else { strncpy(path_out, input, path_out_size - 1U); path_out[path_out_size - 1U] = '\0'; } if (access(path_out, F_OK) == 0) { fprintf(stderr, "%sFile exists: %s%s\n", ANSI_YELLOW, path_out, ANSI_RESET); print_option('o', "verwrite"); print_option('c', "hoose different path"); print_option('q', "uit"); wizard_option_t opts[] = {{'o', ""}, {'c', ""}, {'q', ""}}; char c = read_menu_choice(opts, 3); if (c == 'o') return 0; if (c == 'c') return maybe_prompt_write_path(path_out, path_out_size); return -1; } return 0; } static int new_agent_identity_step(didactyl_config_t* cfg) { for (;;) { render_wizard_page_header("Step 2 of 7", "New Agent Setup -- Identity"); print_option('g', "enerate a new Nostr keypair"); print_option('p', "rovide an existing nsec"); print_option('b', "ack"); print_option('q', "uit"); wizard_option_t opts[] = {{'g', ""}, {'p', ""}, {'b', ""}, {'q', ""}}; char c = read_menu_choice(opts, 4); if (c == 'q') return -1; if (c == 'b') return 1; if (c == 'g') { unsigned char priv[32]; unsigned char pub[32]; if (nostr_generate_keypair(priv, pub) != 0) { fprintf(stderr, "%sFailed to generate keypair.%s\n", ANSI_RED, ANSI_RESET); continue; } memcpy(cfg->keys.private_key, priv, 32); memcpy(cfg->keys.public_key, pub, 32); nostr_bytes_to_hex(cfg->keys.public_key, 32, cfg->keys.public_key_hex); char nsec[OW_MAX_KEY_LEN] = {0}; char npub[OW_MAX_KEY_LEN] = {0}; if (nostr_key_to_bech32(priv, "nsec", nsec) != 0 || nostr_key_to_bech32(pub, "npub", npub) != 0) { fprintf(stderr, "%sFailed to encode generated keypair.%s\n", ANSI_RED, ANSI_RESET); continue; } snprintf(cfg->keys.nsec, sizeof(cfg->keys.nsec), "%s", nsec); fprintf(stderr, "\nGenerated identity:\n"); fprintf(stderr, " npub: %s\n", npub); fprintf(stderr, "%s nsec: %s%s\n", ANSI_YELLOW, nsec, ANSI_RESET); fprintf(stderr, "%sSave this nsec now. It will not be persisted unless you explicitly export with nsec.%s\n", ANSI_YELLOW, ANSI_RESET); char ok[WIZARD_LINE_MAX] = {0}; if (read_line_prompt("Type 'ok' when saved (or q to quit): ", ok, sizeof(ok)) != 0) return -1; if (line_is_quit(ok)) return -1; } if (c == 'p') { char nsec_in[OW_MAX_KEY_LEN] = {0}; if (read_secret_prompt("Enter nsec (nsec1... or 64-char hex): ", nsec_in, sizeof(nsec_in)) != 0) return -1; if (line_is_quit(nsec_in)) return -1; if (derive_keys_from_nsec_local(nsec_in, cfg) != 0) { fprintf(stderr, "%sInvalid nsec.%s\n", ANSI_RED, ANSI_RESET); continue; } char npub[OW_MAX_KEY_LEN] = {0}; if (nostr_key_to_bech32(cfg->keys.public_key, "npub", npub) == 0) { fprintf(stderr, "Derived identity: %s\n", npub); } else { fprintf(stderr, "Derived pubkey (hex): %s\n", cfg->keys.public_key_hex); } } return 0; } } static int new_agent_flow(didactyl_config_t* cfg, char* genesis_path_out, size_t genesis_path_out_size) { (void)genesis_path_out; (void)genesis_path_out_size; char agent_name[OW_MAX_NAME_LEN] = {0}; render_wizard_page_header("Step 1 of 7", "New Agent Setup -- Agent Profile"); if (read_line_prompt(" Agent name [Didactyl]: ", agent_name, sizeof(agent_name)) != 0) return -1; if (line_is_quit(agent_name)) return -1; if (agent_name[0] == '\0') { snprintf(agent_name, sizeof(agent_name), "%s", "Didactyl"); } int rc = new_agent_identity_step(cfg); if (rc != 0) return rc < 0 ? -1 : 1; if (prompt_admin_pubkey(cfg) != 0) return -1; int exists = 0; int admin_name_found = 0; char admin_name[128] = {0}; if (validate_new_agent_identity_and_admin(cfg, &exists, admin_name, sizeof(admin_name), &admin_name_found) != 0) { fprintf(stderr, "%sWarning: Unable to validate identity/admin against Nostr right now.%s\n", ANSI_YELLOW, ANSI_RESET); } else { render_wizard_page_header("Step 4 of 7", "New Agent Setup -- Identity Validation"); fprintf(stderr, " Agent kind 10002: %s\n", exists ? "FOUND (may overwrite existing state)" : "Agent npub available for new setup"); if (admin_name_found) { fprintf(stderr, " Admin profile: %s\n", admin_name); } else { fprintf(stderr, " Admin profile: NOT FOUND (you can still continue)\n"); } fprintf(stderr, "\n"); if (exists) { print_option('c', "ontinue anyway"); print_option('a', "bort"); wizard_option_t confirm[] = {{'c', ""}, {'a', ""}}; char cc = read_menu_choice(confirm, 2); if (cc != 'c') { return -1; } } } if (prompt_llm_config(cfg) != 0) return -1; for (;;) { rc = prompt_relay_configuration(cfg); if (rc < 0) return -1; if (rc == 0) break; if (prompt_llm_config(cfg) != 0) return -1; } if (configure_default_skill_for_agent(cfg, agent_name) != 0) { fprintf(stderr, "%sFailed to prepare default skill event content.%s\n", ANSI_RED, ANSI_RESET); return -1; } if (apply_default_startup_events(cfg, agent_name) != 0) { fprintf(stderr, "%sFailed to prepare startup events from defaults.%s\n", ANSI_RED, ANSI_RESET); return -1; } render_wizard_page_header("Step 7 of 7", "New Agent Setup -- Review"); fprintf(stderr, " Name: %s\n", agent_name); fprintf(stderr, " Identity: %.16s...\n", cfg->keys.public_key_hex); fprintf(stderr, " Admin: %.16s...\n", cfg->admin.pubkey); fprintf(stderr, " LLM: %s @ %s\n", cfg->llm.model, cfg->llm.base_url); fprintf(stderr, " Relays: %d configured\n", cfg->relay_count); fprintf(stderr, " DM Protocol: nip04\n\n"); print_option('b', "oot the agent now"); print_option('i', "nstall dedicated-user systemd service and boot"); print_option('s', "tart over"); print_option('q', "uit"); wizard_option_t opts[] = {{'b', ""}, {'i', ""}, {'s', ""}, {'q', ""}}; char c = read_menu_choice(opts, 4); if (c == 'q') return -1; if (c == 's') return 2; if (c == 'b') { /* Publish kind 30078 agent_config + llm_config so the --nsec-only restart path can recover them. Non-fatal: main() will also publish during bootstrap, but doing it here is belt-and-suspenders. */ if (persist_runtime_config_to_nostr_wizard_online(cfg) != 0) { fprintf(stderr, "%sWarning: failed to publish runtime config to Nostr (will retry on boot).%s\n", ANSI_YELLOW, ANSI_RESET); } fprintf(stderr, "%sStep 7 of 7 -- Booting new agent. Check your messages for initial greeting.%s\n", ANSI_YELLOW, ANSI_RESET); return 0; } if (c == 'i') { /* Publish kind 30078 agent_config + llm_config BEFORE installing the service. The systemd service starts with --nsec only and depends on recovering these from relays. */ if (persist_runtime_config_to_nostr_wizard_online(cfg) != 0) { fprintf(stderr, "%sFailed to publish runtime config to Nostr; refusing install to avoid missing admin/LLM on first boot.%s\n", ANSI_RED, ANSI_RESET); return -1; } if (install_system_service_with_dedicated_user(cfg, agent_name) != 0) { fprintf(stderr, "%sFailed to install dedicated-user system service.%s\n", ANSI_RED, ANSI_RESET); return -1; } char service_user[64] = {0}; sanitize_service_user_from_name(agent_name, service_user, sizeof(service_user)); char service_name[96] = {0}; snprintf(service_name, sizeof(service_name), "%s.service", service_user); sleep(2); char* is_active_argv[] = {"systemctl", "is-active", "--quiet", service_name, NULL}; if (run_privileged_command_local(is_active_argv) == 0) { fprintf(stderr, "%sStep 7 of 7 -- Service %s is active. Setup is complete; this wizard will now exit. " "Expect a startup message in the admin inbox.%s\n", ANSI_YELLOW, service_name, ANSI_RESET); } else { fprintf(stderr, "%sStep 7 of 7 -- Service installed but not active yet. This wizard will now exit. " "Check status with: sudo systemctl status %s ; logs with: sudo journalctl -u %s -f%s\n", ANSI_RED, service_name, service_name, ANSI_RESET); } return 1; } return -1; } static int existing_agent_flow(didactyl_config_t* cfg) { char nsec_in[OW_MAX_KEY_LEN] = {0}; char agent_name[OW_MAX_NAME_LEN] = {0}; int agent_name_found = 0; int relay_changed = 0; render_wizard_page_header("Existing Agent", "Identity"); if (read_secret_prompt("Enter your agent nsec (nsec1... or 64-char hex): ", nsec_in, sizeof(nsec_in)) != 0) return -1; if (line_is_quit(nsec_in)) return -1; if (derive_keys_from_nsec_local(nsec_in, cfg) != 0) { fprintf(stderr, "%sInvalid nsec.%s\n", ANSI_RED, ANSI_RESET); return -1; } int relay_found = 0; if (recover_existing_config_from_nostr(cfg, &relay_found) != 0 || !relay_found) { render_wizard_page_header("Existing Agent", "Config Recovery"); fprintf(stderr, " Relay list (kind 10002): NOT FOUND\n\n"); fprintf(stderr, "%sUnable to find a kind 10002 relay list for this identity.%s\n", ANSI_RED, ANSI_RESET); fprintf(stderr, "\n"); print_option('n', "ew agent setup with this identity"); print_option('q', "uit to main menu"); wizard_option_t opts[] = {{'n', ""}, {'q', ""}}; char c = read_menu_choice(opts, 2); if (c == 'n') { int rc = new_agent_flow(cfg, NULL, 0); if (rc == 0) return SETUP_WIZARD_RC_BOOTSTRAP; if (rc == 1) return SETUP_WIZARD_RC_EXIT; return -1; } return -1; } if (recover_full_config_from_nostr(cfg, agent_name, sizeof(agent_name), &agent_name_found) != 0) { fprintf(stderr, "%sWarning: unable to recover full config from Nostr, continuing with partial recovery.%s\n", ANSI_YELLOW, ANSI_RESET); } if (!agent_name_found || agent_name[0] == '\0') { snprintf(agent_name, sizeof(agent_name), "%s", "Didactyl"); } for (;;) { char masked_key[64] = {0}; size_t api_len = strlen(cfg->llm.api_key); if (api_len >= 8U) { snprintf(masked_key, sizeof(masked_key), "%.4s...%s", cfg->llm.api_key, cfg->llm.api_key + api_len - 4U); } else if (api_len > 0U) { snprintf(masked_key, sizeof(masked_key), "****"); } else { snprintf(masked_key, sizeof(masked_key), "(not set)"); } render_wizard_page_header("Existing Agent", "Recovered Configuration"); fprintf(stderr, "Your agent was found. Change any of the following:\n\n"); fprintf(stderr, " Name: %s\n", agent_name); fprintf(stderr, " Identity: %.16s...\n", cfg->keys.public_key_hex); fprintf(stderr, " Admin: %s\n", cfg->admin.pubkey[0] ? cfg->admin.pubkey : "(not set)"); fprintf(stderr, " LLM Provider: %s\n", cfg->llm.provider[0] ? cfg->llm.provider : "(not set)"); fprintf(stderr, " LLM Model: %s\n", cfg->llm.model[0] ? cfg->llm.model : "(not set)"); fprintf(stderr, " LLM Base URL: %s\n", cfg->llm.base_url[0] ? cfg->llm.base_url : "(not set)"); fprintf(stderr, " LLM API Key: %s\n", masked_key); fprintf(stderr, " Relays: %d configured\n", cfg->relay_count); fprintf(stderr, "\n"); print_option('a', "dmin pubkey"); print_option('l', "lm config"); print_option('r', "elay configuration"); print_option('c', "ontinue to launch options"); print_option('q', "uit"); wizard_option_t opts[] = {{'a', ""}, {'l', ""}, {'r', ""}, {'c', ""}, {'q', ""}}; char c = read_menu_choice(opts, 5); if (c == 'q') { return -1; } if (c == 'c') { break; } if (c == 'a') { if (prompt_admin_pubkey_with_header(cfg, "Existing Agent -- Review", "Existing Agent -- Administrator") != 0) { return -1; } continue; } if (c == 'l') { if (prompt_llm_config_with_header(cfg, "Existing Agent -- Review", "Existing Agent -- LLM Provider") != 0) { return -1; } continue; } if (c == 'r') { for (;;) { int rc = prompt_relay_configuration_with_header(cfg, "Existing Agent -- Review", "Existing Agent -- Relay Configuration"); if (rc < 0) return -1; if (rc == 1) break; relay_changed = 1; break; } continue; } } render_wizard_page_header("Existing Agent", "Launch Options"); fprintf(stderr, " Name: %s\n", agent_name); fprintf(stderr, " Identity: %.16s...\n", cfg->keys.public_key_hex); fprintf(stderr, " Admin: %.16s...\n", cfg->admin.pubkey); fprintf(stderr, " LLM: %s @ %s\n", cfg->llm.model, cfg->llm.base_url); fprintf(stderr, " Relays: %d configured\n\n", cfg->relay_count); print_option('b', "oot the agent now"); print_option('i', "nstall dedicated-user systemd service and boot"); print_option('q', "uit"); wizard_option_t launch_opts[] = {{'b', ""}, {'i', ""}, {'q', ""}}; char lc = read_menu_choice(launch_opts, 3); if (lc == 'q') return -1; if (lc == 'b') { if (relay_changed) { if (ensure_startup_kind10002_from_current_relays(cfg) != 0) { fprintf(stderr, "%sFailed to stage updated relay list for startup publish.%s\n", ANSI_RED, ANSI_RESET); return -1; } } /* Always return BOOTSTRAP so main() re-publishes kind 30078. The wizard has the authoritative config in memory. */ return SETUP_WIZARD_RC_BOOTSTRAP; } if (lc == 'i') { /* Always publish kind 30078 before installing the service, even if config was not changed. The service depends on recovering these from relays and the event may have been purged. */ if (persist_runtime_config_to_nostr_wizard_online(cfg) != 0) { fprintf(stderr, "%sFailed to publish runtime config to Nostr; refusing install to avoid stale admin/LLM on first boot.%s\n", ANSI_RED, ANSI_RESET); return -1; } if (relay_changed) { if (publish_kind10002_relays_wizard_online(cfg) != 0) { fprintf(stderr, "%sFailed to publish updated relay list (kind 10002) to Nostr; refusing install.%s\n", ANSI_RED, ANSI_RESET); return -1; } } if (install_system_service_with_dedicated_user(cfg, agent_name) != 0) { fprintf(stderr, "%sFailed to install dedicated-user system service.%s\n", ANSI_RED, ANSI_RESET); return -1; } char service_user[64] = {0}; sanitize_service_user_from_name(agent_name, service_user, sizeof(service_user)); char service_name[96] = {0}; snprintf(service_name, sizeof(service_name), "%s.service", service_user); sleep(2); char* is_active_argv[] = {"systemctl", "is-active", "--quiet", service_name, NULL}; if (run_privileged_command_local(is_active_argv) == 0) { fprintf(stderr, "%sService %s is active. Setup is complete; this wizard will now exit.%s\n", ANSI_YELLOW, service_name, ANSI_RESET); } else { fprintf(stderr, "%sService installed but not active yet. Check status with: sudo systemctl status %s%s\n", ANSI_RED, service_name, ANSI_RESET); } return SETUP_WIZARD_RC_EXIT; } return -1; } static int load_genesis_path_flow(didactyl_config_t* cfg) { char path[OW_MAX_URL_LEN] = {0}; render_wizard_page_header("Load Genesis File", "Bootstrap from File"); if (read_line_prompt("Path [./genesis.jsonc]: ", path, sizeof(path)) != 0) { return -1; } if (line_is_quit(path)) return -1; if (path[0] == '\0') { snprintf(path, sizeof(path), "%s", "./genesis.jsonc"); } config_free(cfg); if (config_load(path, cfg) != 0) { fprintf(stderr, "%sFailed to load %s: %s%s\n", ANSI_RED, path, config_last_error(), ANSI_RESET); return -1; } if (cfg->keys.nsec[0] == '\0') { char nsec_in[OW_MAX_KEY_LEN] = {0}; if (read_secret_prompt("Config has no nsec. Enter nsec now: ", nsec_in, sizeof(nsec_in)) != 0) return -1; if (derive_keys_from_nsec_local(nsec_in, cfg) != 0) { fprintf(stderr, "%sInvalid nsec.%s\n", ANSI_RED, ANSI_RESET); return -1; } } return 0; } int setup_wizard_run(didactyl_config_t* config, char* genesis_path_out, size_t genesis_path_out_size) { if (!config) return -1; config_set_defaults(config); if (set_default_relays(config) != 0) { return -1; } for (;;) { render_wizard_page_header("Main Menu", "Choose Setup Mode"); print_option('n', "ew agent -- create a fresh identity"); print_option('e', "xisting -- start an already set up agent"); print_option('l', "oad -- boot from genesis.jsonc"); print_option('q', "uit"); wizard_option_t opts[] = {{'n', ""}, {'e', ""}, {'l', ""}, {'q', ""}}; char c = read_menu_choice(opts, 4); if (c == 'q' || c == '\0') { return -1; } if (c == 'l') { int rc = load_genesis_path_flow(config); if (rc == 0) { return SETUP_WIZARD_RC_BOOTSTRAP; } return rc; } if (c == 'e') { int rc = existing_agent_flow(config); if (rc == SETUP_WIZARD_RC_BOOTSTRAP || rc == SETUP_WIZARD_RC_EXISTING || rc == SETUP_WIZARD_RC_EXIT) { return rc; } return rc; } if (c == 'n') { int rc = new_agent_flow(config, genesis_path_out, genesis_path_out_size); if (rc == 2) { config_free(config); config_set_defaults(config); if (set_default_relays(config) != 0) { return -1; } continue; } if (rc == 0) { return SETUP_WIZARD_RC_BOOTSTRAP; } if (rc == 1) { return SETUP_WIZARD_RC_EXIT; } return rc; } } }