#define _POSIX_C_SOURCE 200809L #include "config.h" #include #include #include #include #include #include #include "cjson/cJSON.h" #include "../../nostr_core_lib/nostr_core/nostr_core.h" /* --------------------------------------------------------------------------- * jsonc_strip_comments – strip JSONC comments from a buffer * * Returns a newly malloc'd string containing valid JSON (caller must free). * The function is careful to skip over JSON string literals so that comment * characters inside strings are preserved. Handles: * - single-line comments // (to end of line) * - block comments (slash-star ... star-slash, may span lines) * - escaped quotes inside strings \" * - trailing commas before ] or } are NOT removed (cJSON tolerates them) * * Returns NULL on allocation failure. * ------------------------------------------------------------------------ */ char* jsonc_strip_comments(const char* src, size_t src_len) { if (!src || src_len == 0) { char* empty = (char*)malloc(1); if (empty) empty[0] = '\0'; return empty; } char* out = (char*)malloc(src_len + 1); if (!out) return NULL; size_t o = 0; size_t i = 0; while (i < src_len) { /* Inside a JSON string literal — copy verbatim until closing quote */ if (src[i] == '"') { out[o++] = src[i++]; /* opening quote */ while (i < src_len) { if (src[i] == '\\' && i + 1 < src_len) { out[o++] = src[i++]; /* backslash */ out[o++] = src[i++]; /* escaped char */ } else if (src[i] == '"') { out[o++] = src[i++]; /* closing quote */ break; } else { out[o++] = src[i++]; } } continue; } /* Single-line comment // */ if (src[i] == '/' && i + 1 < src_len && src[i + 1] == '/') { /* skip to end of line */ i += 2; while (i < src_len && src[i] != '\n') { i++; } /* preserve the newline so line numbers stay meaningful */ if (i < src_len) { out[o++] = src[i++]; /* the \n */ } continue; } /* Block comment (slash-star ... star-slash) */ if (src[i] == '/' && i + 1 < src_len && src[i + 1] == '*') { i += 2; while (i + 1 < src_len && !(src[i] == '*' && src[i + 1] == '/')) { /* preserve newlines inside block comments */ if (src[i] == '\n') { out[o++] = '\n'; } i++; } if (i + 1 < src_len) { i += 2; /* skip closing */ } continue; } /* Normal character — copy through */ out[o++] = src[i++]; } out[o] = '\0'; return out; } static char g_config_last_error[512] = {0}; static void config_set_error(const char* fmt, ...) { va_list ap; va_start(ap, fmt); vsnprintf(g_config_last_error, sizeof(g_config_last_error), fmt ? fmt : "unknown configuration error", ap); va_end(ap); } const char* config_last_error(void) { return g_config_last_error[0] != '\0' ? g_config_last_error : "unknown configuration error"; } static int read_file_to_buffer(const char* path, char** out_buf, size_t* out_len) { FILE* fp = fopen(path, "rb"); if (!fp) { return -1; } if (fseek(fp, 0, SEEK_END) != 0) { fclose(fp); return -1; } long len = ftell(fp); if (len < 0) { fclose(fp); return -1; } if (fseek(fp, 0, SEEK_SET) != 0) { fclose(fp); return -1; } char* buf = (char*)malloc((size_t)len + 1U); if (!buf) { fclose(fp); return -1; } size_t read_len = fread(buf, 1, (size_t)len, fp); fclose(fp); if (read_len != (size_t)len) { free(buf); return -1; } buf[len] = '\0'; *out_buf = buf; *out_len = (size_t)len; return 0; } static int copy_json_string(cJSON* obj, const char* key, char* dst, size_t dst_size, int required) { cJSON* item = cJSON_GetObjectItemCaseSensitive(obj, key); if (!item || !cJSON_IsString(item) || !item->valuestring) { return required ? -1 : 0; } size_t n = strlen(item->valuestring); if (n >= dst_size) { return -1; } memcpy(dst, item->valuestring, n + 1U); return 0; } 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_private_key(const char* nsec_or_hex, unsigned char out_private_key[32]) { if (!nsec_or_hex || !out_private_key) { return -1; } if (strncmp(nsec_or_hex, "nsec1", 5) == 0) { return nostr_decode_nsec(nsec_or_hex, out_private_key) == 0 ? 0 : -1; } if (is_hex64(nsec_or_hex)) { return nostr_hex_to_bytes(nsec_or_hex, out_private_key, 32) == 0 ? 0 : -1; } return -1; } static int decode_pubkey_hex_or_npub(const char* in, char out_hex[65]) { if (!in || !out_hex) { return -1; } if (is_hex64(in)) { memcpy(out_hex, in, 65); return 0; } if (strncmp(in, "npub1", 5) == 0) { unsigned char pubkey[32]; if (nostr_decode_npub(in, pubkey) != 0) { return -1; } nostr_bytes_to_hex(pubkey, 32, out_hex); return 0; } return -1; } static int parse_tools_config(cJSON* root, didactyl_config_t* config) { cJSON* tools = cJSON_GetObjectItemCaseSensitive(root, "tools"); if (!tools || !cJSON_IsObject(tools)) { return 0; } cJSON* enabled = cJSON_GetObjectItemCaseSensitive(tools, "enabled"); cJSON* max_turns = cJSON_GetObjectItemCaseSensitive(tools, "max_turns"); cJSON* trigger_max_turns = cJSON_GetObjectItemCaseSensitive(tools, "trigger_max_turns"); cJSON* api_default_max_turns = cJSON_GetObjectItemCaseSensitive(tools, "api_default_max_turns"); cJSON* api_max_turns_ceiling = cJSON_GetObjectItemCaseSensitive(tools, "api_max_turns_ceiling"); cJSON* stall_repeat_threshold = cJSON_GetObjectItemCaseSensitive(tools, "stall_repeat_threshold"); cJSON* local_http_fetch_default_timeout_seconds = cJSON_GetObjectItemCaseSensitive(tools, "local_http_fetch_default_timeout_seconds"); cJSON* local_http_fetch_max_timeout_seconds = cJSON_GetObjectItemCaseSensitive(tools, "local_http_fetch_max_timeout_seconds"); cJSON* blossom_max_upload_bytes = cJSON_GetObjectItemCaseSensitive(tools, "blossom_max_upload_bytes"); cJSON* blossom_max_download_bytes = cJSON_GetObjectItemCaseSensitive(tools, "blossom_max_download_bytes"); if (enabled && cJSON_IsBool(enabled)) { config->tools.enabled = cJSON_IsTrue(enabled) ? 1 : 0; } if (max_turns && cJSON_IsNumber(max_turns)) { config->tools.max_turns = (int)max_turns->valuedouble; } if (trigger_max_turns && cJSON_IsNumber(trigger_max_turns)) { config->tools.trigger_max_turns = (int)trigger_max_turns->valuedouble; } if (api_default_max_turns && cJSON_IsNumber(api_default_max_turns)) { config->tools.api_default_max_turns = (int)api_default_max_turns->valuedouble; } if (api_max_turns_ceiling && cJSON_IsNumber(api_max_turns_ceiling)) { config->tools.api_max_turns_ceiling = (int)api_max_turns_ceiling->valuedouble; } if (stall_repeat_threshold && cJSON_IsNumber(stall_repeat_threshold)) { config->tools.stall_repeat_threshold = (int)stall_repeat_threshold->valuedouble; } if (local_http_fetch_default_timeout_seconds && cJSON_IsNumber(local_http_fetch_default_timeout_seconds)) { config->tools.local_http_fetch_default_timeout_seconds = (int)local_http_fetch_default_timeout_seconds->valuedouble; } if (local_http_fetch_max_timeout_seconds && cJSON_IsNumber(local_http_fetch_max_timeout_seconds)) { config->tools.local_http_fetch_max_timeout_seconds = (int)local_http_fetch_max_timeout_seconds->valuedouble; } if (blossom_max_upload_bytes && cJSON_IsNumber(blossom_max_upload_bytes)) { config->tools.blossom_max_upload_bytes = (int)blossom_max_upload_bytes->valuedouble; } if (blossom_max_download_bytes && cJSON_IsNumber(blossom_max_download_bytes)) { config->tools.blossom_max_download_bytes = (int)blossom_max_download_bytes->valuedouble; } cJSON* shell = cJSON_GetObjectItemCaseSensitive(tools, "shell"); if (!shell || !cJSON_IsObject(shell)) { return 0; } cJSON* shell_enabled = cJSON_GetObjectItemCaseSensitive(shell, "enabled"); cJSON* timeout_seconds = cJSON_GetObjectItemCaseSensitive(shell, "timeout_seconds"); cJSON* max_output_bytes = cJSON_GetObjectItemCaseSensitive(shell, "max_output_bytes"); if (shell_enabled && cJSON_IsBool(shell_enabled)) { config->tools.shell.enabled = cJSON_IsTrue(shell_enabled) ? 1 : 0; } if (timeout_seconds && cJSON_IsNumber(timeout_seconds)) { config->tools.shell.timeout_seconds = (int)timeout_seconds->valuedouble; } if (max_output_bytes && cJSON_IsNumber(max_output_bytes)) { config->tools.shell.max_output_bytes = (int)max_output_bytes->valuedouble; } if (copy_json_string(shell, "working_directory", config->tools.shell.working_directory, sizeof(config->tools.shell.working_directory), 0) != 0) { return -1; } if (config->tools.max_turns < 1) { config->tools.max_turns = 8; } if (config->tools.trigger_max_turns < 1) { config->tools.trigger_max_turns = config->tools.max_turns; } if (config->tools.api_default_max_turns < 1) { config->tools.api_default_max_turns = config->tools.max_turns; } if (config->tools.api_max_turns_ceiling < 1) { config->tools.api_max_turns_ceiling = 16; } if (config->tools.api_default_max_turns > config->tools.api_max_turns_ceiling) { config->tools.api_default_max_turns = config->tools.api_max_turns_ceiling; } if (config->tools.stall_repeat_threshold < 2) { config->tools.stall_repeat_threshold = 3; } if (config->tools.local_http_fetch_default_timeout_seconds < 1) { config->tools.local_http_fetch_default_timeout_seconds = 20; } if (config->tools.local_http_fetch_max_timeout_seconds < 1) { config->tools.local_http_fetch_max_timeout_seconds = 120; } if (config->tools.local_http_fetch_default_timeout_seconds > config->tools.local_http_fetch_max_timeout_seconds) { config->tools.local_http_fetch_default_timeout_seconds = config->tools.local_http_fetch_max_timeout_seconds; } if (config->tools.blossom_max_upload_bytes < 1024) { config->tools.blossom_max_upload_bytes = 16 * 1024 * 1024; } if (config->tools.blossom_max_download_bytes < 1024) { config->tools.blossom_max_download_bytes = 16 * 1024 * 1024; } return 0; } static int parse_security_config(cJSON* root, didactyl_config_t* config) { cJSON* security = cJSON_GetObjectItemCaseSensitive(root, "security"); if (!security || !cJSON_IsObject(security)) { return 0; } cJSON* verify_signatures = cJSON_GetObjectItemCaseSensitive(security, "verify_signatures"); if (verify_signatures && cJSON_IsBool(verify_signatures)) { config->security.verify_signatures = cJSON_IsTrue(verify_signatures) ? 1 : 0; } if (copy_json_string(security, "stranger_response", config->security.stranger_response, sizeof(config->security.stranger_response), 0) != 0) { return -1; } cJSON* tiers = cJSON_GetObjectItemCaseSensitive(security, "tiers"); if (!tiers || !cJSON_IsObject(tiers)) { return 0; } cJSON* admin_tier = cJSON_GetObjectItemCaseSensitive(tiers, "admin"); if (admin_tier && cJSON_IsObject(admin_tier)) { cJSON* tools_enabled = cJSON_GetObjectItemCaseSensitive(admin_tier, "tools_enabled"); if (tools_enabled && cJSON_IsBool(tools_enabled)) { config->security.admin.tools_enabled = cJSON_IsTrue(tools_enabled) ? 1 : 0; } } cJSON* wot_tier = cJSON_GetObjectItemCaseSensitive(tiers, "wot"); if (wot_tier && cJSON_IsObject(wot_tier)) { cJSON* enabled = cJSON_GetObjectItemCaseSensitive(wot_tier, "enabled"); cJSON* tools_enabled = cJSON_GetObjectItemCaseSensitive(wot_tier, "tools_enabled"); if (enabled && cJSON_IsBool(enabled)) { config->security.wot.enabled = cJSON_IsTrue(enabled) ? 1 : 0; } if (tools_enabled && cJSON_IsBool(tools_enabled)) { config->security.wot.tools_enabled = cJSON_IsTrue(tools_enabled) ? 1 : 0; } } cJSON* stranger_tier = cJSON_GetObjectItemCaseSensitive(tiers, "stranger"); if (stranger_tier && cJSON_IsObject(stranger_tier)) { cJSON* enabled = cJSON_GetObjectItemCaseSensitive(stranger_tier, "enabled"); if (enabled && cJSON_IsBool(enabled)) { config->security.stranger.enabled = cJSON_IsTrue(enabled) ? 1 : 0; } } return 0; } static int parse_admin_context_config(cJSON* root, didactyl_config_t* config) { cJSON* admin_context = cJSON_GetObjectItemCaseSensitive(root, "admin_context"); if (!admin_context || !cJSON_IsObject(admin_context)) { return 0; } cJSON* enabled = cJSON_GetObjectItemCaseSensitive(admin_context, "enabled"); if (enabled && cJSON_IsBool(enabled)) { config->admin_context.enabled = cJSON_IsTrue(enabled) ? 1 : 0; } cJSON* kind_1_limit = cJSON_GetObjectItemCaseSensitive(admin_context, "kind_1_limit"); if (kind_1_limit && cJSON_IsNumber(kind_1_limit)) { config->admin_context.kind_1_limit = (int)kind_1_limit->valuedouble; } cJSON* subscribe_kinds = cJSON_GetObjectItemCaseSensitive(admin_context, "subscribe_kinds"); if (subscribe_kinds && cJSON_IsArray(subscribe_kinds)) { config->admin_context.track_kind_0 = 0; config->admin_context.track_kind_3 = 0; config->admin_context.track_kind_10002 = 0; config->admin_context.track_kind_1 = 0; int n = cJSON_GetArraySize(subscribe_kinds); for (int i = 0; i < n; i++) { cJSON* k = cJSON_GetArrayItem(subscribe_kinds, i); if (!k || !cJSON_IsNumber(k)) { continue; } int kind = (int)k->valuedouble; if (kind == 0) config->admin_context.track_kind_0 = 1; else if (kind == 3) config->admin_context.track_kind_3 = 1; else if (kind == 10002) config->admin_context.track_kind_10002 = 1; else if (kind == 1) config->admin_context.track_kind_1 = 1; } } return 0; } static int parse_triggers_config(cJSON* root, didactyl_config_t* config) { cJSON* triggers = cJSON_GetObjectItemCaseSensitive(root, "triggers"); if (!triggers || !cJSON_IsObject(triggers)) { return 0; } cJSON* enabled = cJSON_GetObjectItemCaseSensitive(triggers, "enabled"); cJSON* max_active = cJSON_GetObjectItemCaseSensitive(triggers, "max_active"); cJSON* cooldown_seconds = cJSON_GetObjectItemCaseSensitive(triggers, "cooldown_seconds"); cJSON* llm_rate_limit = cJSON_GetObjectItemCaseSensitive(triggers, "llm_rate_limit_per_minute"); cJSON* template_rate_limit = cJSON_GetObjectItemCaseSensitive(triggers, "template_rate_limit_per_minute"); if (enabled && cJSON_IsBool(enabled)) { config->triggers.enabled = cJSON_IsTrue(enabled) ? 1 : 0; } if (max_active && cJSON_IsNumber(max_active)) { config->triggers.max_active = (int)max_active->valuedouble; } if (cooldown_seconds && cJSON_IsNumber(cooldown_seconds)) { config->triggers.cooldown_seconds = (int)cooldown_seconds->valuedouble; } if (llm_rate_limit && cJSON_IsNumber(llm_rate_limit)) { config->triggers.llm_rate_limit_per_minute = (int)llm_rate_limit->valuedouble; } if (template_rate_limit && cJSON_IsNumber(template_rate_limit)) { config->triggers.template_rate_limit_per_minute = (int)template_rate_limit->valuedouble; } if (config->triggers.max_active < 1) { config->triggers.max_active = 1; } if (config->triggers.cooldown_seconds < 0) { config->triggers.cooldown_seconds = 0; } if (config->triggers.llm_rate_limit_per_minute < 1) { config->triggers.llm_rate_limit_per_minute = 1; } if (config->triggers.template_rate_limit_per_minute < 1) { config->triggers.template_rate_limit_per_minute = 1; } return 0; } static int parse_dm_protocol_config(cJSON* root, didactyl_config_t* config) { if (!root || !config) { return -1; } cJSON* dm_protocol = cJSON_GetObjectItemCaseSensitive(root, "dm_protocol"); if (!dm_protocol) { return 0; } if (!cJSON_IsString(dm_protocol) || !dm_protocol->valuestring) { return -1; } if (strcmp(dm_protocol->valuestring, "nip04") == 0) { config->dm_protocol = DM_PROTOCOL_NIP04; } else if (strcmp(dm_protocol->valuestring, "nip17") == 0) { config->dm_protocol = DM_PROTOCOL_NIP17; } else if (strcmp(dm_protocol->valuestring, "both") == 0) { config->dm_protocol = DM_PROTOCOL_BOTH; } else { return -1; } return 0; } static int parse_api_config(cJSON* root, didactyl_config_t* config) { cJSON* api = cJSON_GetObjectItemCaseSensitive(root, "api"); if (!api || !cJSON_IsObject(api)) { return 0; } cJSON* enabled = cJSON_GetObjectItemCaseSensitive(api, "enabled"); cJSON* port = cJSON_GetObjectItemCaseSensitive(api, "port"); if (enabled && cJSON_IsBool(enabled)) { config->api.enabled = cJSON_IsTrue(enabled) ? 1 : 0; } if (port && cJSON_IsNumber(port)) { config->api.port = (int)port->valuedouble; } if (copy_json_string(api, "bind_address", config->api.bind_address, sizeof(config->api.bind_address), 0) != 0) { return -1; } if (config->api.port < 1 || config->api.port > 65535) { config->api.port = 8484; } if (config->api.bind_address[0] == '\0') { snprintf(config->api.bind_address, sizeof(config->api.bind_address), "%s", "127.0.0.1"); } return 0; } static void free_cashu_wallet_mints(cashu_wallet_config_t* wallet) { if (!wallet || !wallet->mint_urls) { return; } for (int i = 0; i < wallet->mint_count; i++) { free(wallet->mint_urls[i]); } free(wallet->mint_urls); wallet->mint_urls = NULL; wallet->mint_count = 0; } static int parse_cashu_wallet_config(cJSON* root, didactyl_config_t* config) { cJSON* wallet = cJSON_GetObjectItemCaseSensitive(root, "cashu_wallet"); if (!wallet || !cJSON_IsObject(wallet)) { return 0; } cJSON* enabled = cJSON_GetObjectItemCaseSensitive(wallet, "enabled"); if (enabled && cJSON_IsBool(enabled)) { config->cashu_wallet.enabled = cJSON_IsTrue(enabled) ? 1 : 0; } cJSON* auto_load = cJSON_GetObjectItemCaseSensitive(wallet, "auto_load"); if (auto_load && cJSON_IsBool(auto_load)) { config->cashu_wallet.auto_load = cJSON_IsTrue(auto_load) ? 1 : 0; } cJSON* timeout = cJSON_GetObjectItemCaseSensitive(wallet, "mint_timeout_seconds"); if (timeout && cJSON_IsNumber(timeout)) { config->cashu_wallet.mint_timeout_seconds = (int)timeout->valuedouble; } if (copy_json_string(wallet, "unit", config->cashu_wallet.unit, sizeof(config->cashu_wallet.unit), 0) != 0) { return -1; } cJSON* mints = cJSON_GetObjectItemCaseSensitive(wallet, "mints"); if (mints) { if (!cJSON_IsArray(mints)) { return -1; } int count = cJSON_GetArraySize(mints); char** urls = NULL; if (count > 0) { urls = (char**)calloc((size_t)count, sizeof(char*)); if (!urls) { return -1; } } for (int i = 0; i < count; i++) { cJSON* item = cJSON_GetArrayItem(mints, i); if (!item || !cJSON_IsString(item) || !item->valuestring || item->valuestring[0] == '\0') { for (int j = 0; j < i; j++) { free(urls[j]); } free(urls); return -1; } urls[i] = strdup(item->valuestring); if (!urls[i]) { for (int j = 0; j < i; j++) { free(urls[j]); } free(urls); return -1; } } free_cashu_wallet_mints(&config->cashu_wallet); config->cashu_wallet.mint_urls = urls; config->cashu_wallet.mint_count = count; } if (config->cashu_wallet.unit[0] == '\0') { snprintf(config->cashu_wallet.unit, sizeof(config->cashu_wallet.unit), "%s", "sat"); } if (config->cashu_wallet.mint_timeout_seconds < 1) { config->cashu_wallet.mint_timeout_seconds = 30; } return 0; } static cJSON* find_tag_value_string(cJSON* tags, const char* tag_key) { if (!tags || !cJSON_IsArray(tags) || !tag_key) { return NULL; } 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, tag_key) == 0) { return val; } } return NULL; } static int set_tag_value_string(cJSON* tags, const char* tag_key, const char* tag_value) { if (!tags || !cJSON_IsArray(tags) || !tag_key || !tag_value || tag_value[0] == '\0') { return -1; } 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) || !key->valuestring) { continue; } if (strcmp(key->valuestring, tag_key) == 0) { if (cJSON_IsString(val)) { if (!cJSON_SetValuestring(val, tag_value)) { return -1; } return 0; } cJSON* new_val = cJSON_CreateString(tag_value); if (!new_val) { return -1; } cJSON_ReplaceItemInArray(tag, 1, new_val); return 0; } } cJSON* new_tag = cJSON_CreateArray(); if (!new_tag) { return -1; } cJSON_AddItemToArray(new_tag, cJSON_CreateString(tag_key)); cJSON_AddItemToArray(new_tag, cJSON_CreateString(tag_value)); cJSON_AddItemToArray(tags, new_tag); return 0; } static int startup_event_has_d_tag(const startup_event_t* se, const char* d_tag); static int append_startup_event(didactyl_config_t* config, int kind, const char* content, const char* tags_json); static int normalize_skill_d_tag(int event_kind, cJSON* item, cJSON* tags) { if (!item || !tags || !cJSON_IsArray(tags)) { return 0; } if (event_kind != 31123 && event_kind != 31124) { return 0; } cJSON* d_val = find_tag_value_string(tags, "d"); if (!d_val || !cJSON_IsString(d_val) || !d_val->valuestring) { return 0; } int needs_normalize = (strcmp(d_val->valuestring, "skill") == 0 || strcmp(d_val->valuestring, "private_skill") == 0); if (!needs_normalize) { return 0; } const char* d_tag = NULL; if (!d_tag) { cJSON* content_fields = cJSON_GetObjectItemCaseSensitive(item, "content_fields"); if (content_fields && cJSON_IsObject(content_fields)) { cJSON* name = cJSON_GetObjectItemCaseSensitive(content_fields, "name"); if (name && cJSON_IsString(name) && name->valuestring && name->valuestring[0] != '\0') { d_tag = name->valuestring; } } } if (!d_tag) { return 0; } return set_tag_value_string(tags, "d", d_tag); } static int parse_legacy_default_skill_to_startup_events(cJSON* root, didactyl_config_t* config) { if (!root || !config) { return -1; } cJSON* def = cJSON_GetObjectItemCaseSensitive(root, "default_skill"); if (!def || !cJSON_IsObject(def)) { return 0; } cJSON* kind = cJSON_GetObjectItemCaseSensitive(def, "kind"); cJSON* d_tag = cJSON_GetObjectItemCaseSensitive(def, "d_tag"); cJSON* content = cJSON_GetObjectItemCaseSensitive(def, "content"); cJSON* content_fields = cJSON_GetObjectItemCaseSensitive(def, "content_fields"); cJSON* tags = cJSON_GetObjectItemCaseSensitive(def, "tags"); if (!kind || !cJSON_IsNumber(kind) || !d_tag || !cJSON_IsString(d_tag) || !d_tag->valuestring || d_tag->valuestring[0] == '\0') { return -1; } int kind_i = (int)kind->valuedouble; if (kind_i != 31123 && kind_i != 31124) { return -1; } for (int i = 0; i < config->startup_event_count; i++) { startup_event_t* se = &config->startup_events[i]; if (se->kind == kind_i && startup_event_has_d_tag(se, d_tag->valuestring)) { return 0; } } char* content_text = NULL; if (content_fields) { if (!cJSON_IsObject(content_fields)) { return -1; } content_text = cJSON_PrintUnformatted(content_fields); } else { if (!content || !cJSON_IsString(content) || !content->valuestring) { return -1; } content_text = strdup(content->valuestring); } if (!content_text) { return -1; } cJSON* tags_work = NULL; if (tags) { if (!cJSON_IsArray(tags)) { free(content_text); return -1; } tags_work = cJSON_Duplicate(tags, 1); } else { tags_work = cJSON_CreateArray(); } if (!tags_work || !cJSON_IsArray(tags_work)) { cJSON_Delete(tags_work); free(content_text); return -1; } if (set_tag_value_string(tags_work, "d", d_tag->valuestring) != 0) { cJSON_Delete(tags_work); free(content_text); return -1; } char* tags_json = cJSON_PrintUnformatted(tags_work); cJSON_Delete(tags_work); if (!tags_json) { free(content_text); return -1; } int rc = append_startup_event(config, kind_i, content_text, tags_json); free(content_text); free(tags_json); return rc; } static int parse_startup_events(cJSON* root, didactyl_config_t* config) { cJSON* arr = cJSON_GetObjectItemCaseSensitive(root, "startup_events"); if (!arr || !cJSON_IsArray(arr)) { return 0; } int count = cJSON_GetArraySize(arr); if (count <= 0) return 0; config->startup_events = (startup_event_t*)calloc((size_t)count, sizeof(startup_event_t)); if (!config->startup_events) return -1; config->startup_event_count = count; for (int i = 0; i < count; i++) { cJSON* item = cJSON_GetArrayItem(arr, i); if (!item || !cJSON_IsObject(item)) return -1; cJSON* kind = cJSON_GetObjectItemCaseSensitive(item, "kind"); cJSON* content = cJSON_GetObjectItemCaseSensitive(item, "content"); cJSON* content_fields = cJSON_GetObjectItemCaseSensitive(item, "content_fields"); cJSON* tags = cJSON_GetObjectItemCaseSensitive(item, "tags"); if (!kind || !cJSON_IsNumber(kind)) { return -1; } config->startup_events[i].kind = (int)kind->valuedouble; if (content_fields) { if (!cJSON_IsObject(content_fields)) { return -1; } config->startup_events[i].content = cJSON_PrintUnformatted(content_fields); if (!config->startup_events[i].content) { return -1; } } else { if (!content || !cJSON_IsString(content) || !content->valuestring) { return -1; } config->startup_events[i].content = strdup(content->valuestring); if (!config->startup_events[i].content) { return -1; } } if (tags) { if (!cJSON_IsArray(tags)) return -1; if (normalize_skill_d_tag(config->startup_events[i].kind, item, tags) != 0) { return -1; } config->startup_events[i].tags_json = cJSON_PrintUnformatted(tags); if (!config->startup_events[i].tags_json) return -1; } } return 0; } static int parse_relays_from_startup_kind10002(didactyl_config_t* config) { if (!config || !config->startup_events || config->startup_event_count <= 0) { return -1; } for (int i = 0; i < config->startup_event_count; i++) { startup_event_t* se = &config->startup_events[i]; if (se->kind != 10002 || !se->tags_json) { continue; } cJSON* tags = cJSON_Parse(se->tags_json); if (!tags || !cJSON_IsArray(tags)) { cJSON_Delete(tags); continue; } int tag_count = cJSON_GetArraySize(tags); int relay_count = 0; for (int j = 0; j < tag_count; j++) { cJSON* tag = cJSON_GetArrayItem(tags, j); 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') { relay_count++; } } if (relay_count <= 0) { cJSON_Delete(tags); continue; } config->relays = (char**)calloc((size_t)relay_count, sizeof(char*)); if (!config->relays) { cJSON_Delete(tags); return -1; } config->relay_count = relay_count; int out_i = 0; for (int j = 0; j < tag_count && out_i < relay_count; j++) { cJSON* tag = cJSON_GetArrayItem(tags, j); 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') { config->relays[out_i] = strdup(val->valuestring); if (!config->relays[out_i]) { cJSON_Delete(tags); return -1; } out_i++; } } cJSON_Delete(tags); return 0; } return -1; } static int set_default_bootstrap_relays(didactyl_config_t* config) { if (!config) { return -1; } static const char* defaults[] = { "wss://relay.damus.io", "wss://relay.primal.net", "wss://nos.lol" }; const int relay_count = (int)(sizeof(defaults) / sizeof(defaults[0])); char** relays = (char**)calloc((size_t)relay_count, sizeof(char*)); if (!relays) { return -1; } for (int i = 0; i < relay_count; i++) { relays[i] = strdup(defaults[i]); if (!relays[i]) { for (int j = 0; j < i; j++) { free(relays[j]); } free(relays); return -1; } } if (config->relays) { for (int i = 0; i < config->relay_count; i++) { free(config->relays[i]); } free(config->relays); } config->relays = relays; config->relay_count = relay_count; return 0; } static int parse_relays(cJSON* root, didactyl_config_t* config) { (void)root; if (parse_relays_from_startup_kind10002(config) == 0) { return 0; } return set_default_bootstrap_relays(config); } static int startup_event_has_d_tag(const startup_event_t* se, const char* d_tag) { if (!se || !se->tags_json || !d_tag || d_tag[0] == '\0') { return 0; } cJSON* tags = cJSON_Parse(se->tags_json); if (!tags || !cJSON_IsArray(tags)) { cJSON_Delete(tags); return 0; } int found = 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* k = cJSON_GetArrayItem(tag, 0); cJSON* v = cJSON_GetArrayItem(tag, 1); if (!k || !v || !cJSON_IsString(k) || !cJSON_IsString(v) || !k->valuestring || !v->valuestring) continue; if (strcmp(k->valuestring, "d") == 0 && strcmp(v->valuestring, d_tag) == 0) { found = 1; break; } } cJSON_Delete(tags); return found; } static int adoption_tags_has_addr(cJSON* tags, const char* addr) { if (!tags || !cJSON_IsArray(tags) || !addr || addr[0] == '\0') { return 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* k = cJSON_GetArrayItem(tag, 0); cJSON* v = cJSON_GetArrayItem(tag, 1); if (!k || !v || !cJSON_IsString(k) || !cJSON_IsString(v) || !k->valuestring || !v->valuestring) continue; if (strcmp(k->valuestring, "a") == 0 && strcmp(v->valuestring, addr) == 0) { return 1; } } return 0; } static int append_adoption_addr_tag(cJSON* tags, const char* addr) { if (!tags || !cJSON_IsArray(tags) || !addr || addr[0] == '\0') { return -1; } cJSON* tag = cJSON_CreateArray(); if (!tag) return -1; cJSON_AddItemToArray(tag, cJSON_CreateString("a")); cJSON_AddItemToArray(tag, cJSON_CreateString(addr)); cJSON_AddItemToArray(tags, tag); return 0; } static int append_startup_event(didactyl_config_t* config, int kind, const char* content, const char* tags_json) { if (!config || !content) { return -1; } int old_count = config->startup_event_count; startup_event_t* grown = (startup_event_t*)realloc(config->startup_events, (size_t)(old_count + 1) * sizeof(startup_event_t)); if (!grown) { return -1; } config->startup_events = grown; memset(&config->startup_events[old_count], 0, sizeof(startup_event_t)); config->startup_events[old_count].kind = kind; config->startup_events[old_count].content = strdup(content); if (!config->startup_events[old_count].content) { return -1; } if (tags_json && tags_json[0] != '\0') { config->startup_events[old_count].tags_json = strdup(tags_json); if (!config->startup_events[old_count].tags_json) { free(config->startup_events[old_count].content); config->startup_events[old_count].content = NULL; return -1; } } config->startup_event_count = old_count + 1; return 0; } int config_ensure_startup_skill_adoption(didactyl_config_t* config) { if (!config || config->keys.public_key_hex[0] == '\0') { return 0; } cJSON* needed_addrs = cJSON_CreateArray(); if (!needed_addrs) { return -1; } for (int i = 0; i < config->startup_event_count; i++) { startup_event_t* se = &config->startup_events[i]; if (!se || (se->kind != 31123 && se->kind != 31124) || !se->tags_json) { continue; } cJSON* tags = cJSON_Parse(se->tags_json); if (!tags || !cJSON_IsArray(tags)) { cJSON_Delete(tags); continue; } cJSON* d_val = find_tag_value_string(tags, "d"); if (d_val && cJSON_IsString(d_val) && d_val->valuestring && d_val->valuestring[0] != '\0') { char addr[256]; snprintf(addr, sizeof(addr), "%d:%s:%s", se->kind, config->keys.public_key_hex, d_val->valuestring); cJSON_AddItemToArray(needed_addrs, cJSON_CreateString(addr)); } cJSON_Delete(tags); } if (cJSON_GetArraySize(needed_addrs) == 0) { cJSON_Delete(needed_addrs); return 0; } for (int i = 0; i < config->startup_event_count; i++) { startup_event_t* se = &config->startup_events[i]; if (se->kind != 10123 || !se->tags_json) { continue; } cJSON* tags = cJSON_Parse(se->tags_json); if (!tags || !cJSON_IsArray(tags)) { cJSON_Delete(tags); continue; } int needed_n = cJSON_GetArraySize(needed_addrs); for (int j = 0; j < needed_n; j++) { cJSON* addr_j = cJSON_GetArrayItem(needed_addrs, j); if (!addr_j || !cJSON_IsString(addr_j) || !addr_j->valuestring || addr_j->valuestring[0] == '\0') { continue; } if (!adoption_tags_has_addr(tags, addr_j->valuestring) && append_adoption_addr_tag(tags, addr_j->valuestring) != 0) { cJSON_Delete(tags); cJSON_Delete(needed_addrs); return -1; } } char* updated = cJSON_PrintUnformatted(tags); cJSON_Delete(tags); if (!updated) { cJSON_Delete(needed_addrs); return -1; } free(se->tags_json); se->tags_json = updated; cJSON_Delete(needed_addrs); return 0; } cJSON* tags = cJSON_CreateArray(); if (!tags) { cJSON_Delete(needed_addrs); return -1; } int needed_n = cJSON_GetArraySize(needed_addrs); for (int j = 0; j < needed_n; j++) { cJSON* addr_j = cJSON_GetArrayItem(needed_addrs, j); if (!addr_j || !cJSON_IsString(addr_j) || !addr_j->valuestring || addr_j->valuestring[0] == '\0') { continue; } if (append_adoption_addr_tag(tags, addr_j->valuestring) != 0) { cJSON_Delete(tags); cJSON_Delete(needed_addrs); return -1; } } cJSON* app = cJSON_CreateArray(); cJSON* scope = cJSON_CreateArray(); if (!app || !scope) { cJSON_Delete(app); cJSON_Delete(scope); cJSON_Delete(tags); cJSON_Delete(needed_addrs); return -1; } cJSON_AddItemToArray(app, cJSON_CreateString("app")); cJSON_AddItemToArray(app, cJSON_CreateString("didactyl")); cJSON_AddItemToArray(scope, cJSON_CreateString("scope")); cJSON_AddItemToArray(scope, cJSON_CreateString("private")); cJSON_AddItemToArray(tags, app); cJSON_AddItemToArray(tags, scope); char* tags_json = cJSON_PrintUnformatted(tags); cJSON_Delete(tags); cJSON_Delete(needed_addrs); if (!tags_json) { return -1; } int rc = append_startup_event(config, 10123, "", tags_json); free(tags_json); return rc; } void config_free(didactyl_config_t* config) { if (!config) { return; } if (config->relays) { for (int i = 0; i < config->relay_count; i++) { free(config->relays[i]); } free(config->relays); } if (config->startup_events) { for (int i = 0; i < config->startup_event_count; i++) { free(config->startup_events[i].content); free(config->startup_events[i].tags_json); } free(config->startup_events); } free_cashu_wallet_mints(&config->cashu_wallet); memset(config, 0, sizeof(*config)); } int config_load(const char* path, didactyl_config_t* config) { if (!path || !config) { config_set_error("config_load called with invalid arguments"); return -1; } config_set_error("unknown configuration error"); memset(config, 0, sizeof(*config)); snprintf(config->config_path, sizeof(config->config_path), "%s", path); config->dm_protocol = DM_PROTOCOL_NIP04; config->tools.enabled = 1; config->tools.max_turns = 20; config->tools.trigger_max_turns = 12; config->tools.api_default_max_turns = 8; config->tools.api_max_turns_ceiling = 32; config->tools.stall_repeat_threshold = 3; config->tools.local_http_fetch_default_timeout_seconds = 20; config->tools.local_http_fetch_max_timeout_seconds = 120; config->tools.blossom_max_upload_bytes = 16 * 1024 * 1024; config->tools.blossom_max_download_bytes = 16 * 1024 * 1024; config->tools.shell.enabled = 1; config->tools.shell.timeout_seconds = 30; config->tools.shell.max_output_bytes = 65536; strcpy(config->tools.shell.working_directory, "."); config->security.verify_signatures = 1; config->security.admin.enabled = 1; config->security.admin.tools_enabled = 1; config->security.wot.enabled = 1; config->security.wot.tools_enabled = 0; config->security.stranger.enabled = 1; config->security.stranger.tools_enabled = 0; config->security.stranger_response[0] = '\0'; config->admin_context.enabled = 1; config->admin_context.track_kind_0 = 1; config->admin_context.track_kind_3 = 1; config->admin_context.track_kind_10002 = 1; config->admin_context.track_kind_1 = 1; config->admin_context.kind_1_limit = 10; config->triggers.enabled = 1; config->triggers.max_active = 16; config->triggers.cooldown_seconds = 60; config->triggers.llm_rate_limit_per_minute = 10; config->triggers.template_rate_limit_per_minute = 60; config->api.enabled = 0; config->api.port = 8484; snprintf(config->api.bind_address, sizeof(config->api.bind_address), "%s", "127.0.0.1"); config->cashu_wallet.enabled = 0; config->cashu_wallet.mint_urls = NULL; config->cashu_wallet.mint_count = 0; snprintf(config->cashu_wallet.unit, sizeof(config->cashu_wallet.unit), "%s", "sat"); config->cashu_wallet.auto_load = 1; config->cashu_wallet.mint_timeout_seconds = 30; char* raw_buf = NULL; size_t raw_len = 0; if (read_file_to_buffer(path, &raw_buf, &raw_len) != 0) { if (errno == ENOENT) { if (set_default_bootstrap_relays(config) != 0) { config_set_error("failed to initialize default bootstrap relays"); return -1; } return 0; } config_set_error("failed to read config file '%s': %s", path, strerror(errno)); return -1; } /* Strip JSONC comments before parsing */ char* json_buf = jsonc_strip_comments(raw_buf, raw_len); free(raw_buf); if (!json_buf) { config_set_error("failed to allocate memory for JSONC comment stripping"); return -1; } size_t json_len = strlen(json_buf); cJSON* root = cJSON_ParseWithLength(json_buf, json_len); free(json_buf); if (!root) { const char* err = cJSON_GetErrorPtr(); config_set_error("invalid JSON in config '%s'%s%s", path, err ? " near: " : "", err ? err : ""); return -1; } int rc = -1; cJSON* keys = cJSON_GetObjectItemCaseSensitive(root, "keys"); if (!keys || !cJSON_IsObject(keys)) { keys = cJSON_GetObjectItemCaseSensitive(root, "key"); } cJSON* admin = cJSON_GetObjectItemCaseSensitive(root, "admin"); cJSON* llm = cJSON_GetObjectItemCaseSensitive(root, "llm"); if (!admin || !cJSON_IsObject(admin) || !llm || !cJSON_IsObject(llm)) { config_set_error("config must include object sections: admin, llm"); goto cleanup; } if (!keys || !cJSON_IsObject(keys)) { config->keys.nsec[0] = '\0'; } else { if (copy_json_string(keys, "nsec", config->keys.nsec, sizeof(config->keys.nsec), 0) != 0) { config_set_error("keys.nsec/key.nsec must be a string when provided"); goto cleanup; } } char admin_key_raw[OW_MAX_KEY_LEN] = {0}; if (copy_json_string(admin, "pubkey", admin_key_raw, sizeof(admin_key_raw), 1) != 0) { config_set_error("admin.pubkey is required and must be a non-empty string"); goto cleanup; } if (decode_pubkey_hex_or_npub(admin_key_raw, config->admin.pubkey) != 0) { config_set_error("admin.pubkey must be npub1... or 64-char hex"); goto cleanup; } if (parse_startup_events(root, config) != 0) { config_set_error("invalid startup_events configuration"); goto cleanup; } if (parse_legacy_default_skill_to_startup_events(root, config) != 0) { config_set_error("invalid legacy default_skill configuration"); goto cleanup; } if (parse_relays(root, config) != 0) { config_set_error("relay configuration is invalid and bootstrap fallback failed"); goto cleanup; } if (copy_json_string(llm, "provider", config->llm.provider, sizeof(config->llm.provider), 0) != 0) { config_set_error("llm.provider must be a string if provided"); goto cleanup; } if (config->llm.provider[0] == '\0') { strcpy(config->llm.provider, "openai"); } if (copy_json_string(llm, "api_key", config->llm.api_key, sizeof(config->llm.api_key), 1) != 0) { config_set_error("llm.api_key is required and must be a string"); goto cleanup; } if (copy_json_string(llm, "model", config->llm.model, sizeof(config->llm.model), 1) != 0) { config_set_error("llm.model is required and must be a string"); goto cleanup; } if (copy_json_string(llm, "base_url", config->llm.base_url, sizeof(config->llm.base_url), 1) != 0) { config_set_error("llm.base_url is required and must be a string"); goto cleanup; } cJSON* max_tokens = cJSON_GetObjectItemCaseSensitive(llm, "max_tokens"); cJSON* temperature = cJSON_GetObjectItemCaseSensitive(llm, "temperature"); config->llm.max_tokens = (max_tokens && cJSON_IsNumber(max_tokens)) ? (int)max_tokens->valuedouble : 512; config->llm.temperature = (temperature && cJSON_IsNumber(temperature)) ? temperature->valuedouble : 0.7; if (parse_dm_protocol_config(root, config) != 0) { config_set_error("invalid dm_protocol configuration (expected 'nip04', 'nip17', or 'both')"); goto cleanup; } if (parse_tools_config(root, config) != 0) { config_set_error("invalid tools configuration"); goto cleanup; } if (parse_security_config(root, config) != 0) { config_set_error("invalid security configuration"); goto cleanup; } if (parse_admin_context_config(root, config) != 0) { config_set_error("invalid admin_context configuration"); goto cleanup; } if (parse_triggers_config(root, config) != 0) { config_set_error("invalid triggers configuration"); goto cleanup; } if (parse_api_config(root, config) != 0) { config_set_error("invalid api configuration"); goto cleanup; } if (parse_cashu_wallet_config(root, config) != 0) { config_set_error("invalid cashu_wallet configuration"); goto cleanup; } if (config->keys.nsec[0] != '\0') { if (decode_private_key(config->keys.nsec, config->keys.private_key) != 0) { config_set_error("keys.nsec/key.nsec must be valid nsec1... or 64-char hex private key"); goto cleanup; } if (nostr_ec_public_key_from_private_key(config->keys.private_key, config->keys.public_key) != 0) { config_set_error("failed to derive public key from keys.nsec/key.nsec"); goto cleanup; } nostr_bytes_to_hex(config->keys.public_key, 32, config->keys.public_key_hex); } rc = 0; cleanup: cJSON_Delete(root); if (rc != 0) { config_free(config); } return rc; }