#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 = 8; cfg->tools.trigger_max_turns = 6; cfg->tools.api_default_max_turns = 4; cfg->tools.api_max_turns_ceiling = 16; 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 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(didactyl_config_t* cfg) { char input[WIZARD_LINE_MAX]; for (;;) { render_wizard_page_header("Step 3 of 7", "New Agent Setup -- Administrator"); 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_llm_config(didactyl_config_t* cfg) { for (;;) { render_wizard_page_header("Step 5 of 7", "New Agent Setup -- LLM Provider"); 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_relay_configuration(didactyl_config_t* cfg) { char input[WIZARD_LINE_MAX]; for (;;) { render_wizard_page_header("Step 6 of 7", "New Agent Setup -- Relay Configuration"); 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 ensure_dir_exists_local(const char* path) { if (!path || path[0] == '\0') return -1; if (mkdir(path, 0755) == 0 || errno == EEXIST) return 0; return -1; } 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 copy_file_local(const char* src, const char* dst) { if (!src || !dst || src[0] == '\0' || dst[0] == '\0') return -1; FILE* in = fopen(src, "rb"); if (!in) return -1; FILE* out = fopen(dst, "wb"); if (!out) { fclose(in); return -1; } char buf[16384]; size_t n = 0; while ((n = fread(buf, 1, sizeof(buf), in)) > 0) { if (fwrite(buf, 1, n, out) != n) { fclose(in); fclose(out); return -1; } } fclose(in); fclose(out); return 0; } 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); FILE* fp = fopen(sudoers_path, "w"); if (!fp) { fprintf(stderr, "%sFailed to write sudoers file at %s.%s\n", ANSI_RED, sudoers_path, ANSI_RESET); return -1; } fprintf(fp, "# 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); fclose(fp); if (chmod(sudoers_path, 0440) != 0) { fprintf(stderr, "%sFailed to chmod 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, "%sSystem install requires root.%s\n", ANSI_RED, ANSI_RESET); fprintf(stderr, " You can open a root shell now and then re-run this install step.\n"); print_option('o', "pen root shell now (sudo -i)"); print_option('r', "eturn without installing"); wizard_option_t root_opts[] = {{'o', ""}, {'r', ""}}; char root_choice = read_menu_choice(root_opts, 2); if (root_choice == 'o') { char* sudo_argv[] = {"sudo", "-i", NULL}; if (run_command_local(sudo_argv) != 0) { fprintf(stderr, "%sFailed to open root shell via sudo.%s\n", ANSI_RED, ANSI_RESET); } else { fprintf(stderr, "%sReturned from root shell.%s\n", ANSI_YELLOW, ANSI_RESET); } fprintf(stderr, " Re-run didactyl as root and choose install again to complete system service setup.\n"); } 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_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; } if (ensure_dir_exists_local(home) != 0) { fprintf(stderr, "%sUnable to create/access home directory: %s%s\n", ANSI_RED, home, 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); if (copy_file_local(self_path, bin_path) != 0) { fprintf(stderr, "%sFailed to copy binary to %s.%s\n", ANSI_RED, bin_path, ANSI_RESET); return -1; } if (chmod(bin_path, 0755) != 0) { fprintf(stderr, "%sFailed to chmod binary at %s.%s\n", ANSI_RED, bin_path, ANSI_RESET); return -1; } /* --- Set ownership on home directory and installed binary --- */ if (chown(home, pw->pw_uid, pw->pw_gid) != 0 || chmod(home, 0750) != 0 || chown(bin_path, pw->pw_uid, pw->pw_gid) != 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); FILE* svc_fp = fopen(service_path, "w"); if (!svc_fp) { fprintf(stderr, "%sFailed to write service file at %s.%s\n", ANSI_RED, service_path, ANSI_RESET); return -1; } fprintf(svc_fp, "[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", service_user, service_user, service_user, home, bin_path, cfg->keys.nsec); if (ca_bundle) { fprintf(svc_fp, "Environment=SSL_CERT_FILE=%s\n", ca_bundle); } fprintf(svc_fp, "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, home); fclose(svc_fp); /* --- 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_command_local(reload_argv) != 0 || run_command_local(enable_argv) != 0 || run_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') { 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') { 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; } fprintf(stderr, "%sStep 7 of 7 -- Booting new agent. Check your messages for initial greeting.%s\n", ANSI_YELLOW, ANSI_RESET); return 0; } return -1; } static int existing_agent_flow(didactyl_config_t* cfg) { char nsec_in[OW_MAX_KEY_LEN] = {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); return -1; } render_wizard_page_header("Existing Agent", "Config Recovery"); fprintf(stderr, " Relay list (kind 10002): FOUND (%d relays)\n\n", cfg->relay_count); return 0; } 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 == 0) { return SETUP_WIZARD_RC_EXISTING; } 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; } } }