760 lines
24 KiB
C
760 lines
24 KiB
C
#define _POSIX_C_SOURCE 200809L
|
|
|
|
#include "config.h"
|
|
|
|
#include <ctype.h>
|
|
#include <errno.h>
|
|
#include <stdarg.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#include "cjson/cJSON.h"
|
|
#include "../../nostr_core_lib/nostr_core/nostr_core.h"
|
|
|
|
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");
|
|
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;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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 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 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* slug = NULL;
|
|
cJSON* slug_val = find_tag_value_string(tags, "slug");
|
|
if (slug_val && cJSON_IsString(slug_val) && slug_val->valuestring && slug_val->valuestring[0] != '\0') {
|
|
slug = slug_val->valuestring;
|
|
}
|
|
|
|
if (!slug) {
|
|
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') {
|
|
slug = name->valuestring;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!slug) {
|
|
return 0;
|
|
}
|
|
|
|
return set_tag_value_string(tags, "d", slug);
|
|
}
|
|
|
|
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 parse_relays(cJSON* root, didactyl_config_t* config) {
|
|
(void)root;
|
|
return parse_relays_from_startup_kind10002(config);
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
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->tools.enabled = 1;
|
|
config->tools.max_turns = 8;
|
|
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;
|
|
|
|
char* json_buf = NULL;
|
|
size_t json_len = 0;
|
|
if (read_file_to_buffer(path, &json_buf, &json_len) != 0) {
|
|
config_set_error("failed to read config file '%s': %s", path, strerror(errno));
|
|
return -1;
|
|
}
|
|
|
|
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");
|
|
cJSON* admin = cJSON_GetObjectItemCaseSensitive(root, "admin");
|
|
cJSON* llm = cJSON_GetObjectItemCaseSensitive(root, "llm");
|
|
|
|
if (!keys || !cJSON_IsObject(keys) ||
|
|
!admin || !cJSON_IsObject(admin) ||
|
|
!llm || !cJSON_IsObject(llm)) {
|
|
config_set_error("config must include object sections: keys, admin, llm");
|
|
goto cleanup;
|
|
}
|
|
|
|
if (copy_json_string(keys, "nsec", config->keys.nsec, sizeof(config->keys.nsec), 1) != 0) {
|
|
config_set_error("keys.nsec is required and must be a non-empty string");
|
|
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_relays(root, config) != 0) {
|
|
config_set_error("relay configuration is invalid: startup_events must include kind 10002 with non-empty r tags");
|
|
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_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 (decode_private_key(config->keys.nsec, config->keys.private_key) != 0) {
|
|
config_set_error("keys.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");
|
|
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;
|
|
}
|