#define _POSIX_C_SOURCE 200809L #include "http_api.h" #include #include #include #include #include #include "agent.h" #include "llm.h" #include "main.h" #include "nostr_handler.h" #include "trigger_manager.h" #include "tools/tools.h" #include "cjson/cJSON.h" #include "debug.h" #include "mongoose.h" static http_api_context_t g_api_ctx; static int g_api_initialized = 0; static struct mg_mgr g_mgr; static struct mg_connection* g_listener = NULL; static int g_api_tls_enabled = 0; static struct mg_str g_tls_cert = {NULL, 0}; static struct mg_str g_tls_key = {NULL, 0}; static const char* k_json_headers = "Content-Type: application/json\r\n" "Access-Control-Allow-Origin: *\r\n" "Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS\r\n" "Access-Control-Allow-Headers: Content-Type\r\n" "Access-Control-Allow-Private-Network: true\r\n"; static int method_is(const struct mg_http_message* hm, const char* method) { size_t n = strlen(method); return hm && hm->method.len == n && strncmp(hm->method.buf, method, n) == 0; } static void reply_json(struct mg_connection* c, int status, cJSON* root) { char* out = cJSON_PrintUnformatted(root); if (!out) { mg_http_reply(c, 500, k_json_headers, "{\"success\":false,\"error\":\"json serialization failed\"}"); return; } mg_http_reply(c, status, k_json_headers, "%s", out); free(out); } static void reply_error(struct mg_connection* c, int status, const char* err) { cJSON* root = cJSON_CreateObject(); if (!root) { mg_http_reply(c, 500, k_json_headers, "{\"success\":false,\"error\":\"oom\"}"); return; } cJSON_AddBoolToObject(root, "success", 0); cJSON_AddStringToObject(root, "error", err ? err : "error"); reply_json(c, status, root); cJSON_Delete(root); } static cJSON* parse_body_json(const struct mg_http_message* hm) { if (!hm || hm->body.len == 0) { return cJSON_CreateObject(); } char* buf = (char*)malloc(hm->body.len + 1U); if (!buf) { return NULL; } memcpy(buf, hm->body.buf, hm->body.len); buf[hm->body.len] = '\0'; cJSON* root = cJSON_Parse(buf); free(buf); return root; } static int estimate_tokens_from_chars(int chars) { if (chars <= 0) return 0; return (chars + 3) / 4; } static uint64_t fnv1a64_local(const char* s) { uint64_t h = 1469598103934665603ULL; if (!s) return h; while (*s) { h ^= (unsigned char)(*s++); h *= 1099511628211ULL; } return h; } static uint64_t tool_calls_fingerprint_local(const llm_response_t* resp) { if (!resp || resp->tool_call_count <= 0 || !resp->tool_calls) { return 1469598103934665603ULL; } uint64_t h = 1469598103934665603ULL; for (int i = 0; i < resp->tool_call_count; i++) { const llm_tool_call_t* tc = &resp->tool_calls[i]; uint64_t name_h = fnv1a64_local(tc->name ? tc->name : ""); uint64_t args_h = fnv1a64_local(tc->arguments_json ? tc->arguments_json : "{}"); uint64_t part = name_h ^ (args_h + 0x9e3779b97f4a7c15ULL + (name_h << 6) + (name_h >> 2)); h ^= part + (uint64_t)(i + 1) * 1099511628211ULL; h *= 1099511628211ULL; } h ^= (uint64_t)resp->tool_call_count; h *= 1099511628211ULL; return h; } static void notify_admin_limit_diagnostic_http(const char* source, const char* reason, const char* subject, int max_turns, int turns_run, int stall_repeat_threshold, int repeated_tool_turns, int current_max_tokens, const char* possible_cause, const char* hint, const char* final_answer) { if (!g_api_ctx.cfg || g_api_ctx.cfg->admin.pubkey[0] == '\0') { return; } char diag[1800]; snprintf(diag, sizeof(diag), "⚠️ Didactyl limit diagnostic\n" "source=%s\n" "reason=%s\n" "subject=%s\n" "max_turns=%d\n" "turns_run=%d\n" "stall_repeat_threshold=%d\n" "repeated_tool_turns=%d\n" "current_max_tokens=%d\n" "possible_cause=%s\n" "hint=%s\n" "final_answer_preview=%.*s", source ? source : "unknown", reason ? reason : "unknown", subject ? subject : "n/a", max_turns, turns_run, stall_repeat_threshold, repeated_tool_turns, current_max_tokens, possible_cause ? possible_cause : "n/a", hint ? hint : "n/a", 360, final_answer ? final_answer : ""); (void)nostr_handler_send_dm_auto(g_api_ctx.cfg->admin.pubkey, diag); } static int uri_extract_after_prefix(const struct mg_str* uri, const char* prefix, char* out, size_t out_sz) { if (!uri || !prefix || !out || out_sz == 0) { return -1; } out[0] = '\0'; size_t prefix_len = strlen(prefix); if (uri->len <= prefix_len || strncmp(uri->buf, prefix, prefix_len) != 0) { return -1; } size_t rem = uri->len - prefix_len; if (rem == 0 || rem >= out_sz) { return -1; } memcpy(out, uri->buf + prefix_len, rem); out[rem] = '\0'; return 0; } static void handle_trigger_webhook(struct mg_connection* c, const struct mg_http_message* hm, const char* d_tag) { if (!g_api_ctx.trigger_manager) { reply_error(c, 503, "trigger manager unavailable"); return; } active_trigger_t trigger; if (trigger_manager_find(g_api_ctx.trigger_manager, d_tag, &trigger) != 0) { reply_error(c, 404, "active trigger not found for d_tag"); return; } if (trigger.trigger_type != TRIGGER_TYPE_WEBHOOK) { reply_error(c, 400, "trigger type is not webhook"); return; } if (!trigger.enabled) { reply_error(c, 400, "webhook trigger is disabled"); return; } cJSON* payload = parse_body_json(hm); if (!payload || !cJSON_IsObject(payload)) { cJSON_Delete(payload); reply_error(c, 400, "invalid JSON body"); return; } cJSON* event = cJSON_CreateObject(); if (!event) { cJSON_Delete(payload); reply_error(c, 500, "oom"); return; } cJSON_AddStringToObject(event, "type", "webhook"); cJSON_AddStringToObject(event, "skill_d_tag", d_tag); cJSON_AddNumberToObject(event, "created_at", (double)time(NULL)); cJSON_AddItemToObject(event, "payload", payload); int fired = trigger_manager_fire(g_api_ctx.trigger_manager, d_tag, event, "webhook"); cJSON_Delete(event); if (fired < 0) { reply_error(c, 404, "active trigger not found for d_tag"); return; } cJSON* out = cJSON_CreateObject(); if (!out) { reply_error(c, 500, "oom"); return; } cJSON_AddBoolToObject(out, "success", 1); cJSON_AddStringToObject(out, "d_tag", d_tag); cJSON_AddBoolToObject(out, "fired", fired > 0 ? 1 : 0); reply_json(c, 200, out); cJSON_Delete(out); } static char* maybe_model_override_begin(cJSON* body, llm_config_t* out_old_cfg, int* out_overridden) { if (!body || !out_old_cfg || !out_overridden) return NULL; *out_overridden = 0; cJSON* model = cJSON_GetObjectItemCaseSensitive(body, "model"); if (!model || !cJSON_IsString(model) || !model->valuestring || model->valuestring[0] == '\0') { return NULL; } if (llm_get_config(out_old_cfg) != 0) { return "failed to read current llm config"; } llm_config_t next = *out_old_cfg; snprintf(next.model, sizeof(next.model), "%s", model->valuestring); if (llm_set_config(&next) != 0) { return "failed to set temporary model"; } *out_overridden = 1; return NULL; } static void maybe_model_override_end(const llm_config_t* old_cfg, int overridden) { if (overridden && old_cfg) { (void)llm_set_config(old_cfg); } } static const char* classify_part_name(cJSON* msg, int idx) { return agent_classify_message_part(msg, idx); } static int build_context_parts_response(cJSON* out_root) { if (!out_root) return -1; char* messages_json = NULL; if (agent_build_admin_messages_json("", DIDACTYL_SENDER_ADMIN, &messages_json) != 0 || !messages_json) { return -1; } cJSON* arr = cJSON_Parse(messages_json); free(messages_json); if (!arr || !cJSON_IsArray(arr)) { cJSON_Delete(arr); return -1; } cJSON* parts = cJSON_CreateArray(); if (!parts) { cJSON_Delete(arr); return -1; } int total_chars = 0; int n = cJSON_GetArraySize(arr); for (int i = 0; i < n; i++) { cJSON* msg = cJSON_GetArrayItem(arr, i); if (!msg || !cJSON_IsObject(msg)) continue; cJSON* role = cJSON_GetObjectItemCaseSensitive(msg, "role"); cJSON* content = cJSON_GetObjectItemCaseSensitive(msg, "content"); const char* role_s = (role && cJSON_IsString(role) && role->valuestring) ? role->valuestring : "unknown"; const char* content_s = (content && cJSON_IsString(content) && content->valuestring) ? content->valuestring : ""; int chars = (int)strlen(content_s); total_chars += chars; cJSON* part = cJSON_CreateObject(); if (!part) continue; cJSON_AddStringToObject(part, "name", classify_part_name(msg, i)); cJSON_AddStringToObject(part, "role", role_s); cJSON_AddNumberToObject(part, "chars", chars); cJSON_AddNumberToObject(part, "estimated_tokens", estimate_tokens_from_chars(chars)); cJSON_AddStringToObject(part, "content", content_s); cJSON_AddItemToArray(parts, part); } cJSON_AddBoolToObject(out_root, "success", 1); cJSON_AddNumberToObject(out_root, "total_chars", total_chars); cJSON_AddNumberToObject(out_root, "total_estimated_tokens", estimate_tokens_from_chars(total_chars)); cJSON_AddItemToObject(out_root, "parts", parts); cJSON_AddItemToObject(out_root, "messages", arr); return 0; } static int append_simple_message_local(cJSON* messages, const char* role, const char* content) { if (!messages || !role) return -1; cJSON* msg = cJSON_CreateObject(); if (!msg) return -1; cJSON_AddStringToObject(msg, "role", role); cJSON_AddStringToObject(msg, "content", content ? content : ""); cJSON_AddItemToArray(messages, msg); return 0; } static int append_assistant_tool_calls_message_local(cJSON* messages, const llm_response_t* resp) { if (!messages || !resp || resp->tool_call_count <= 0) return -1; cJSON* assistant = cJSON_CreateObject(); cJSON* tool_calls = cJSON_CreateArray(); if (!assistant || !tool_calls) { cJSON_Delete(assistant); cJSON_Delete(tool_calls); return -1; } cJSON_AddStringToObject(assistant, "role", "assistant"); if (resp->content) cJSON_AddStringToObject(assistant, "content", resp->content); else cJSON_AddNullToObject(assistant, "content"); for (int i = 0; i < resp->tool_call_count; i++) { const llm_tool_call_t* tc = &resp->tool_calls[i]; cJSON* tc_obj = cJSON_CreateObject(); cJSON* fn_obj = cJSON_CreateObject(); if (!tc_obj || !fn_obj) { cJSON_Delete(tc_obj); cJSON_Delete(fn_obj); cJSON_Delete(assistant); cJSON_Delete(tool_calls); return -1; } cJSON_AddStringToObject(tc_obj, "id", tc->id ? tc->id : ""); cJSON_AddStringToObject(tc_obj, "type", "function"); cJSON_AddStringToObject(fn_obj, "name", tc->name ? tc->name : ""); cJSON_AddStringToObject(fn_obj, "arguments", tc->arguments_json ? tc->arguments_json : "{}"); cJSON_AddItemToObject(tc_obj, "function", fn_obj); cJSON_AddItemToArray(tool_calls, tc_obj); } cJSON_AddItemToObject(assistant, "tool_calls", tool_calls); cJSON_AddItemToArray(messages, assistant); return 0; } static int append_tool_result_message_local(cJSON* messages, const char* tool_call_id, const char* tool_result_json) { if (!messages || !tool_call_id) return -1; cJSON* msg = cJSON_CreateObject(); if (!msg) return -1; cJSON_AddStringToObject(msg, "role", "tool"); cJSON_AddStringToObject(msg, "tool_call_id", tool_call_id); cJSON_AddStringToObject(msg, "content", tool_result_json ? tool_result_json : "{\"success\":false,\"error\":\"empty tool result\"}"); cJSON_AddItemToArray(messages, msg); return 0; } static int is_space_char_local(char c) { return c == ' ' || c == '\t' || c == '\n' || c == '\r'; } static const char* skip_spaces_local(const char* s) { while (s && *s && is_space_char_local(*s)) s++; return s ? s : ""; } static char* json_error_string_local(const char* msg) { cJSON* out = cJSON_CreateObject(); if (!out) return NULL; cJSON_AddBoolToObject(out, "success", 0); cJSON_AddStringToObject(out, "error", msg ? msg : "error"); char* json = cJSON_PrintUnformatted(out); cJSON_Delete(out); return json; } static char* build_slash_args_json_local(const char* raw_args) { const char* s = skip_spaces_local(raw_args); if (!s || s[0] == '\0') { return strdup("{}"); } cJSON* parsed = cJSON_Parse(s); if (parsed) { char* out = cJSON_PrintUnformatted(parsed); cJSON_Delete(parsed); return out; } cJSON* obj = cJSON_CreateObject(); if (!obj) return NULL; cJSON_AddStringToObject(obj, "input", s); char* out = cJSON_PrintUnformatted(obj); cJSON_Delete(obj); return out; } static int append_textf_http(char** buf, size_t* cap, size_t* used, const char* fmt, ...) { if (!buf || !cap || !used || !fmt) return -1; while (1) { if (*used + 128U >= *cap) { size_t next_cap = (*cap) * 2U; char* grown = (char*)realloc(*buf, next_cap); if (!grown) return -1; *buf = grown; *cap = next_cap; } va_list ap; va_start(ap, fmt); int n = vsnprintf(*buf + *used, *cap - *used, fmt, ap); va_end(ap); if (n < 0) return -1; if ((size_t)n < (*cap - *used)) { *used += (size_t)n; return 0; } size_t next_cap = (*cap) * 2U + (size_t)n + 64U; char* grown = (char*)realloc(*buf, next_cap); if (!grown) return -1; *buf = grown; *cap = next_cap; } } static int append_markdown_indent_http(char** buf, size_t* cap, size_t* used, int depth) { for (int i = 0; i < depth; i++) { if (append_textf_http(buf, cap, used, " ") != 0) { return -1; } } return 0; } static int append_cjson_markdown_http(char** buf, size_t* cap, size_t* used, cJSON* node, int depth) { if (!node) { return append_markdown_indent_http(buf, cap, used, depth) == 0 && append_textf_http(buf, cap, used, "- null\n") == 0 ? 0 : -1; } if (cJSON_IsObject(node)) { cJSON* child = NULL; cJSON_ArrayForEach(child, node) { const char* key = child->string ? child->string : "item"; if (cJSON_IsString(child)) { if (append_markdown_indent_http(buf, cap, used, depth) != 0 || append_textf_http(buf, cap, used, "- **%s:** %s\n", key, child->valuestring ? child->valuestring : "") != 0) { return -1; } } else if (cJSON_IsBool(child)) { if (append_markdown_indent_http(buf, cap, used, depth) != 0 || append_textf_http(buf, cap, used, "- **%s:** %s\n", key, cJSON_IsTrue(child) ? "true" : "false") != 0) { return -1; } } else if (cJSON_IsNumber(child)) { if (append_markdown_indent_http(buf, cap, used, depth) != 0 || append_textf_http(buf, cap, used, "- **%s:** %g\n", key, child->valuedouble) != 0) { return -1; } } else if (cJSON_IsNull(child)) { if (append_markdown_indent_http(buf, cap, used, depth) != 0 || append_textf_http(buf, cap, used, "- **%s:** null\n", key) != 0) { return -1; } } else { if (append_markdown_indent_http(buf, cap, used, depth) != 0 || append_textf_http(buf, cap, used, "- **%s:**\n", key) != 0) { return -1; } if (append_cjson_markdown_http(buf, cap, used, child, depth + 1) != 0) { return -1; } } } return 0; } if (cJSON_IsArray(node)) { int n = cJSON_GetArraySize(node); for (int i = 0; i < n; i++) { cJSON* item = cJSON_GetArrayItem(node, i); if (cJSON_IsString(item)) { if (append_markdown_indent_http(buf, cap, used, depth) != 0 || append_textf_http(buf, cap, used, "- %s\n", item->valuestring ? item->valuestring : "") != 0) { return -1; } } else if (cJSON_IsBool(item)) { if (append_markdown_indent_http(buf, cap, used, depth) != 0 || append_textf_http(buf, cap, used, "- %s\n", cJSON_IsTrue(item) ? "true" : "false") != 0) { return -1; } } else if (cJSON_IsNumber(item)) { if (append_markdown_indent_http(buf, cap, used, depth) != 0 || append_textf_http(buf, cap, used, "- %g\n", item->valuedouble) != 0) { return -1; } } else if (cJSON_IsNull(item)) { if (append_markdown_indent_http(buf, cap, used, depth) != 0 || append_textf_http(buf, cap, used, "- null\n") != 0) { return -1; } } else { if (append_markdown_indent_http(buf, cap, used, depth) != 0 || append_textf_http(buf, cap, used, "-\n") != 0) { return -1; } if (append_cjson_markdown_http(buf, cap, used, item, depth + 1) != 0) { return -1; } } } return 0; } if (cJSON_IsString(node)) { return append_markdown_indent_http(buf, cap, used, depth) == 0 && append_textf_http(buf, cap, used, "- %s\n", node->valuestring ? node->valuestring : "") == 0 ? 0 : -1; } if (cJSON_IsBool(node)) { return append_markdown_indent_http(buf, cap, used, depth) == 0 && append_textf_http(buf, cap, used, "- %s\n", cJSON_IsTrue(node) ? "true" : "false") == 0 ? 0 : -1; } if (cJSON_IsNumber(node)) { return append_markdown_indent_http(buf, cap, used, depth) == 0 && append_textf_http(buf, cap, used, "- %g\n", node->valuedouble) == 0 ? 0 : -1; } return append_markdown_indent_http(buf, cap, used, depth) == 0 && append_textf_http(buf, cap, used, "- null\n") == 0 ? 0 : -1; } static char* format_result_markdown_http(const char* raw_result) { if (!raw_result) { return strdup("No output."); } cJSON* root = cJSON_Parse(raw_result); if (!root) { return strdup(raw_result); } size_t cap = strlen(raw_result) * 2U + 512U; if (cap < 2048U) cap = 2048U; size_t used = 0U; char* out = (char*)malloc(cap); if (!out) { cJSON_Delete(root); return strdup(raw_result); } out[0] = '\0'; if (append_cjson_markdown_http(&out, &cap, &used, root, 0) != 0) { cJSON_Delete(root); free(out); return strdup(raw_result); } cJSON_Delete(root); return out; } static char* build_slash_help_all_json_local(void) { size_t cap = 2048U; size_t used = 0U; char* out = (char*)malloc(cap); if (!out) return NULL; out[0] = '\0'; if (append_textf_http(&out, &cap, &used, "AVAILABLE TOOLS\n") != 0) { free(out); return NULL; } char* tool_list_json = tools_execute(g_api_ctx.tools_ctx, "tool_list", "{}"); cJSON* tool_root = tool_list_json ? cJSON_Parse(tool_list_json) : NULL; cJSON* tools = tool_root ? cJSON_GetObjectItemCaseSensitive(tool_root, "tools") : NULL; if (!tools || !cJSON_IsArray(tools) || cJSON_GetArraySize(tools) <= 0) { (void)append_textf_http(&out, &cap, &used, "- (none)\n"); } else { int tn = cJSON_GetArraySize(tools); int* used_idx = (int*)calloc((size_t)tn, sizeof(int)); if (!used_idx) { cJSON_Delete(tool_root); free(tool_list_json); free(out); return NULL; } for (int k = 0; k < tn; k++) { int best = -1; const char* best_name = NULL; for (int i = 0; i < tn; i++) { if (used_idx[i]) continue; cJSON* row = cJSON_GetArrayItem(tools, i); cJSON* name = row ? cJSON_GetObjectItemCaseSensitive(row, "name") : NULL; const char* name_s = (name && cJSON_IsString(name) && name->valuestring) ? name->valuestring : "unknown"; if (best < 0 || strcmp(name_s, best_name) < 0) { best = i; best_name = name_s; } } if (best < 0) { break; } used_idx[best] = 1; cJSON* row = cJSON_GetArrayItem(tools, best); cJSON* name = row ? cJSON_GetObjectItemCaseSensitive(row, "name") : NULL; cJSON* desc = row ? cJSON_GetObjectItemCaseSensitive(row, "description") : NULL; const char* name_s = (name && cJSON_IsString(name) && name->valuestring) ? name->valuestring : "unknown"; const char* desc_s = (desc && cJSON_IsString(desc) && desc->valuestring) ? desc->valuestring : ""; if (append_textf_http(&out, &cap, &used, "- %s%s%s\n", name_s, desc_s[0] ? " — " : "", desc_s) != 0) { free(used_idx); cJSON_Delete(tool_root); free(tool_list_json); free(out); return NULL; } } free(used_idx); } cJSON_Delete(tool_root); free(tool_list_json); if (append_textf_http(&out, &cap, &used, "\nAVAILABLE SKILLS\n") != 0) { free(out); return NULL; } char* skill_list_json = tools_execute(g_api_ctx.tools_ctx, "skill_list", "{}"); cJSON* skill_root = skill_list_json ? cJSON_Parse(skill_list_json) : NULL; cJSON* skills = skill_root ? cJSON_GetObjectItemCaseSensitive(skill_root, "skills") : NULL; int available_count = 0; if (skills && cJSON_IsArray(skills) && cJSON_GetArraySize(skills) > 0) { int sn = cJSON_GetArraySize(skills); for (int i = 0; i < sn; i++) { cJSON* row = cJSON_GetArrayItem(skills, i); cJSON* owner = row ? cJSON_GetObjectItemCaseSensitive(row, "owner") : NULL; cJSON* d_tag = row ? cJSON_GetObjectItemCaseSensitive(row, "d_tag") : NULL; cJSON* desc = row ? cJSON_GetObjectItemCaseSensitive(row, "description") : NULL; const char* owner_s = (owner && cJSON_IsString(owner) && owner->valuestring) ? owner->valuestring : ""; const char* d_tag_s = (d_tag && cJSON_IsString(d_tag) && d_tag->valuestring) ? d_tag->valuestring : NULL; const char* desc_s = (desc && cJSON_IsString(desc) && desc->valuestring) ? desc->valuestring : ""; if (strcmp(owner_s, "agent") != 0 || !d_tag_s || d_tag_s[0] == '\0') { continue; } if (append_textf_http(&out, &cap, &used, "- %s%s%s\n", d_tag_s, desc_s[0] ? " — " : "", desc_s) != 0) { cJSON_Delete(skill_root); free(skill_list_json); free(out); return NULL; } available_count++; } } if (available_count <= 0) { (void)append_textf_http(&out, &cap, &used, "- (none)\n"); } cJSON_Delete(skill_root); free(skill_list_json); if (append_textf_http(&out, &cap, &used, "\nADOPTED SKILLS\n") != 0) { free(out); return NULL; } char* adopted_json = tools_execute(g_api_ctx.tools_ctx, "adopted_skills", "{}"); cJSON* adopted_root = adopted_json ? cJSON_Parse(adopted_json) : NULL; cJSON* adoption_events_content = adopted_root ? cJSON_GetObjectItemCaseSensitive(adopted_root, "content") : NULL; cJSON* adoption_events_json = adopted_root ? cJSON_GetObjectItemCaseSensitive(adopted_root, "adoption_events_json") : NULL; const char* adoption_events_text = NULL; if (adoption_events_content && cJSON_IsString(adoption_events_content) && adoption_events_content->valuestring) { adoption_events_text = adoption_events_content->valuestring; } else if (adoption_events_json && cJSON_IsString(adoption_events_json) && adoption_events_json->valuestring) { adoption_events_text = adoption_events_json->valuestring; } cJSON* adoption_events = adoption_events_text ? cJSON_Parse(adoption_events_text) : NULL; int adopted_count = 0; if (adoption_events && cJSON_IsArray(adoption_events) && cJSON_GetArraySize(adoption_events) > 0) { cJSON* ev0 = cJSON_GetArrayItem(adoption_events, 0); cJSON* tags = ev0 ? cJSON_GetObjectItemCaseSensitive(ev0, "tags") : NULL; if (tags && cJSON_IsArray(tags)) { int tn = cJSON_GetArraySize(tags); for (int i = 0; i < tn; 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) || !k->valuestring || !cJSON_IsString(v) || !v->valuestring) { continue; } if (strcmp(k->valuestring, "a") != 0) { continue; } const char* addr = v->valuestring; const char* d_tag = strrchr(addr, ':'); const char* label = (d_tag && d_tag[1] != '\0') ? (d_tag + 1) : addr; if (append_textf_http(&out, &cap, &used, "- %s\n", label) != 0) { cJSON_Delete(adoption_events); cJSON_Delete(adopted_root); free(adopted_json); free(out); return NULL; } adopted_count++; } } } if (adopted_count <= 0) { (void)append_textf_http(&out, &cap, &used, "- (none)\n"); } cJSON_Delete(adoption_events); cJSON_Delete(adopted_root); free(adopted_json); return out; } static char* build_slash_help_tool_json_local(const char* tool_name) { if (!tool_name || tool_name[0] == '\0') { return json_error_string_local("missing tool name"); } char* schema_json = tools_build_openai_schema_json(g_api_ctx.tools_ctx); if (!schema_json) return json_error_string_local("failed to build tool schema"); cJSON* schema = cJSON_Parse(schema_json); free(schema_json); if (!schema || !cJSON_IsArray(schema)) { cJSON_Delete(schema); return json_error_string_local("tool schema parse failure"); } cJSON* matched_fn = NULL; int n = cJSON_GetArraySize(schema); for (int i = 0; i < n; i++) { cJSON* item = cJSON_GetArrayItem(schema, i); cJSON* fn = item ? cJSON_GetObjectItemCaseSensitive(item, "function") : NULL; cJSON* name = fn ? cJSON_GetObjectItemCaseSensitive(fn, "name") : NULL; if (name && cJSON_IsString(name) && name->valuestring && strcmp(name->valuestring, tool_name) == 0) { matched_fn = fn; break; } } if (!matched_fn) { cJSON_Delete(schema); return json_error_string_local("unknown tool"); } cJSON* out = cJSON_CreateObject(); if (!out) { cJSON_Delete(schema); return NULL; } cJSON_AddBoolToObject(out, "success", 1); cJSON_AddStringToObject(out, "mode", "slash_help_tool"); cJSON_AddStringToObject(out, "tool", tool_name); cJSON_AddItemToObject(out, "schema", cJSON_Duplicate(matched_fn, 1)); char* result = cJSON_PrintUnformatted(out); cJSON_Delete(out); cJSON_Delete(schema); return result; } static char* execute_slash_command_http(const char* message) { if (!message || message[0] != '/') return NULL; const char* p = message + 1; while (*p && !is_space_char_local(*p)) p++; size_t tool_len = (size_t)(p - (message + 1)); if (tool_len == 0 || tool_len >= 128U) { return strdup("{\"success\":false,\"error\":\"invalid slash command\"}"); } char tool_name[128]; memcpy(tool_name, message + 1, tool_len); tool_name[tool_len] = '\0'; const char* raw_args = skip_spaces_local(p); char* result_json = NULL; if (raw_args && (strcmp(raw_args, "-h") == 0 || strcmp(raw_args, "--help") == 0)) { result_json = build_slash_help_tool_json_local(tool_name); } else if (strcmp(tool_name, "help") == 0) { if (!raw_args || raw_args[0] == '\0') { result_json = build_slash_help_all_json_local(); } else { char help_tool[128]; size_t i = 0; while (raw_args[i] && !is_space_char_local(raw_args[i]) && i < sizeof(help_tool) - 1U) { help_tool[i] = raw_args[i]; i++; } help_tool[i] = '\0'; result_json = build_slash_help_tool_json_local(help_tool); } } else { char* args_json = build_slash_args_json_local(raw_args); if (!args_json) { result_json = json_error_string_local("failed to parse slash args"); } else { result_json = tools_execute(g_api_ctx.tools_ctx, tool_name, args_json); free(args_json); } } if (!result_json) { result_json = strdup("{\"success\":false,\"error\":\"direct tool execution failed\"}"); } char* markdown_result = format_result_markdown_http(result_json); const char* dm_payload = markdown_result ? markdown_result : result_json; size_t log_cap = strlen(message) + strlen(result_json ? result_json : "") + strlen(dm_payload ? dm_payload : "") + 192U; char* log_payload = (char*)malloc(log_cap); if (log_payload) { snprintf(log_payload, log_cap, "slash=%s\nresult_json=%s\nresult_markdown=%s", message, result_json ? result_json : "", dm_payload ? dm_payload : ""); agent_append_context_log("http_api_agent", "direct_tool_exec", log_payload); free(log_payload); } free(result_json); return markdown_result ? markdown_result : strdup("Command executed with no output."); } static cJSON* run_prompt_with_tools_convo(cJSON* convo, int max_turns, const char* log_sender, const char* log_phase, const char* tool_limit_message) { if (!convo || !cJSON_IsArray(convo)) return NULL; int max_ceiling = 16; if (g_api_ctx.cfg && g_api_ctx.cfg->tools.api_max_turns_ceiling > 0) { max_ceiling = g_api_ctx.cfg->tools.api_max_turns_ceiling; } if (max_turns < 1) max_turns = 1; if (max_turns > max_ceiling) max_turns = max_ceiling; char* tools_json = tools_build_openai_schema_json(g_api_ctx.tools_ctx); if (!tools_json) { return NULL; } cJSON* root = cJSON_CreateObject(); cJSON* turns = cJSON_CreateArray(); if (!root || !turns) { cJSON_Delete(root); cJSON_Delete(turns); free(tools_json); return NULL; } int stall_repeat_threshold = 3; if (g_api_ctx.cfg && g_api_ctx.cfg->tools.stall_repeat_threshold > 1) { stall_repeat_threshold = g_api_ctx.cfg->tools.stall_repeat_threshold; } char* final_response = NULL; uint64_t last_tool_fp = 0; int repeated_tool_turns = 0; int exited_on_stall = 0; int stall_due_to_length = 0; int turns_run = 0; for (int turn = 0; turn < max_turns; turn++) { turns_run = turn + 1; char* messages_json = cJSON_PrintUnformatted(convo); if (!messages_json) break; agent_append_context_log(log_sender ? log_sender : "http_api", log_phase ? log_phase : "llm_chat_with_tools_messages_http_api", messages_json); llm_response_t resp; int rc = llm_chat_with_tools_messages(messages_json, tools_json, "auto", &resp); free(messages_json); if (rc != 0) { final_response = strdup("LLM request failed."); break; } cJSON* turn_obj = cJSON_CreateObject(); if (turn_obj) { cJSON_AddNumberToObject(turn_obj, "turn", turn + 1); cJSON* tool_calls = cJSON_CreateArray(); if (tool_calls) cJSON_AddItemToObject(turn_obj, "tool_calls", tool_calls); cJSON_AddItemToArray(turns, turn_obj); } if (resp.tool_call_count <= 0) { final_response = strdup(resp.content ? resp.content : "No response content."); llm_response_free(&resp); break; } uint64_t current_tool_fp = tool_calls_fingerprint_local(&resp); if (turn == 0 || current_tool_fp != last_tool_fp) { repeated_tool_turns = 1; last_tool_fp = current_tool_fp; } else { repeated_tool_turns++; } if (repeated_tool_turns >= stall_repeat_threshold) { exited_on_stall = 1; stall_due_to_length = (resp.finish_reason && strcmp(resp.finish_reason, "length") == 0) ? 1 : 0; llm_response_free(&resp); break; } if (append_assistant_tool_calls_message_local(convo, &resp) != 0) { llm_response_free(&resp); final_response = strdup("Failed to append assistant tool calls."); break; } cJSON* current_turn = cJSON_GetArrayItem(turns, cJSON_GetArraySize(turns) - 1); cJSON* turn_tool_calls = current_turn ? cJSON_GetObjectItemCaseSensitive(current_turn, "tool_calls") : NULL; for (int i = 0; i < resp.tool_call_count; i++) { llm_tool_call_t* tc = &resp.tool_calls[i]; char* tool_result = tools_execute(g_api_ctx.tools_ctx, tc->name, tc->arguments_json); if (!tool_result) { tool_result = strdup("{\"success\":false,\"error\":\"tool execution failed\"}"); } if (turn_tool_calls && cJSON_IsArray(turn_tool_calls)) { cJSON* tr = cJSON_CreateObject(); if (tr) { cJSON_AddStringToObject(tr, "name", tc->name ? tc->name : ""); cJSON_AddStringToObject(tr, "arguments", tc->arguments_json ? tc->arguments_json : "{}"); cJSON_AddStringToObject(tr, "result", tool_result ? tool_result : "{}"); cJSON_AddItemToArray(turn_tool_calls, tr); } } if (append_tool_result_message_local(convo, tc->id ? tc->id : "", tool_result ? tool_result : "{\"success\":false,\"error\":\"tool execution failed\"}") != 0) { free(tool_result); llm_response_free(&resp); final_response = strdup("Failed to append tool result."); goto done; } free(tool_result); } llm_response_free(&resp); } done:; int hit_max_turn_limit = (!final_response && !exited_on_stall && turns_run >= max_turns); if (!final_response) { char* messages_json = cJSON_PrintUnformatted(convo); if (messages_json) { llm_response_t final_resp; int final_rc = llm_chat_with_tools_messages(messages_json, tools_json, "none", &final_resp); free(messages_json); if (final_rc == 0) { const char* forced_answer = final_resp.content ? final_resp.content : ""; if (forced_answer[0] != '\0') { final_response = strdup(forced_answer); } llm_response_free(&final_resp); } } } if (!final_response) { final_response = strdup(exited_on_stall ? "Stopped repeated tool calls after detecting a loop, and no final summary was produced." : (tool_limit_message ? tool_limit_message : "I hit my tool-use limit for this prompt run.")); } if (exited_on_stall || hit_max_turn_limit) { notify_admin_limit_diagnostic_http("http_api_tool_loop", exited_on_stall ? "stall_repetition_detected" : "max_turns_exhausted", log_sender, max_turns, turns_run, stall_repeat_threshold, repeated_tool_turns, g_api_ctx.cfg ? g_api_ctx.cfg->llm.max_tokens : 0, (exited_on_stall && stall_due_to_length) ? "max_tokens_truncation" : NULL, (exited_on_stall && stall_due_to_length) ? "Increase max_tokens (example: /model_set {\"max_tokens\": 4096})" : NULL, final_response ? final_response : ""); } cJSON_AddBoolToObject(root, "success", 1); cJSON_AddStringToObject(root, "final_response", final_response ? final_response : ""); cJSON_AddItemToObject(root, "turns", turns); llm_config_t cfg; if (llm_get_config(&cfg) == 0) { cJSON_AddStringToObject(root, "model_used", cfg.model); } int input_chars = 0; char* convo_json = cJSON_PrintUnformatted(convo); if (convo_json) { input_chars = (int)strlen(convo_json); free(convo_json); } cJSON_AddNumberToObject(root, "total_input_tokens_estimate", estimate_tokens_from_chars(input_chars)); cJSON_AddNumberToObject(root, "total_output_tokens_estimate", estimate_tokens_from_chars((int)strlen(final_response ? final_response : ""))); free(final_response); free(tools_json); return root; } static cJSON* run_prompt_with_tools(cJSON* body) { if (!body || !cJSON_IsObject(body)) return NULL; cJSON* messages_node = cJSON_GetObjectItemCaseSensitive(body, "messages"); if (!messages_node || !cJSON_IsArray(messages_node)) return NULL; cJSON* convo = cJSON_Duplicate(messages_node, 1); if (!convo || !cJSON_IsArray(convo)) { cJSON_Delete(convo); return NULL; } int max_turns = 4; if (g_api_ctx.cfg && g_api_ctx.cfg->tools.api_default_max_turns > 0) { max_turns = g_api_ctx.cfg->tools.api_default_max_turns; } cJSON* max_turns_node = cJSON_GetObjectItemCaseSensitive(body, "max_turns"); if (max_turns_node && cJSON_IsNumber(max_turns_node)) { max_turns = (int)max_turns_node->valuedouble; } cJSON* root = run_prompt_with_tools_convo(convo, max_turns, "http_api", "llm_chat_with_tools_messages_http_api", "I hit my tool-use limit for this prompt run."); cJSON_Delete(convo); return root; } static void handle_status(struct mg_connection* c) { cJSON* root = cJSON_CreateObject(); if (!root) { reply_error(c, 500, "oom"); return; } cJSON_AddBoolToObject(root, "success", 1); cJSON_AddStringToObject(root, "name", DIDACTYL_NAME); cJSON_AddStringToObject(root, "version", DIDACTYL_VERSION); if (g_api_ctx.cfg) { cJSON_AddStringToObject(root, "pubkey", g_api_ctx.cfg->keys.public_key_hex); cJSON_AddNumberToObject(root, "relay_count", g_api_ctx.cfg->relay_count); } cJSON_AddNumberToObject(root, "connected_relays", nostr_handler_connected_relay_count()); if (g_api_ctx.trigger_manager) { cJSON_AddNumberToObject(root, "active_triggers", trigger_manager_active_count(g_api_ctx.trigger_manager)); } reply_json(c, 200, root); cJSON_Delete(root); } static void handle_context_current(struct mg_connection* c) { char* messages_json = NULL; if (agent_build_admin_messages_json("", DIDACTYL_SENDER_ADMIN, &messages_json) != 0 || !messages_json) { reply_error(c, 500, "failed to build context messages"); return; } cJSON* messages = cJSON_Parse(messages_json); cJSON* root = cJSON_CreateObject(); if (!root || !messages || !cJSON_IsArray(messages)) { free(messages_json); cJSON_Delete(messages); cJSON_Delete(root); reply_error(c, 500, "failed to serialize context"); return; } int chars = (int)strlen(messages_json); cJSON_AddBoolToObject(root, "success", 1); cJSON_AddNumberToObject(root, "total_chars", chars); cJSON_AddNumberToObject(root, "total_estimated_tokens", estimate_tokens_from_chars(chars)); cJSON_AddItemToObject(root, "messages", messages); reply_json(c, 200, root); free(messages_json); cJSON_Delete(root); } static void handle_context_parts(struct mg_connection* c) { cJSON* root = cJSON_CreateObject(); if (!root) { reply_error(c, 500, "oom"); return; } if (build_context_parts_response(root) != 0) { cJSON_Delete(root); reply_error(c, 500, "failed to build context parts"); return; } reply_json(c, 200, root); cJSON_Delete(root); } static void handle_prompt_run_simple(struct mg_connection* c, const struct mg_http_message* hm) { cJSON* body = parse_body_json(hm); if (!body || !cJSON_IsObject(body)) { cJSON_Delete(body); reply_error(c, 400, "invalid JSON body"); return; } cJSON* system = cJSON_GetObjectItemCaseSensitive(body, "system"); cJSON* user = cJSON_GetObjectItemCaseSensitive(body, "user"); if (!system || !user || !cJSON_IsString(system) || !cJSON_IsString(user) || !system->valuestring || !user->valuestring) { cJSON_Delete(body); reply_error(c, 400, "system and user strings are required"); return; } llm_config_t old_cfg; int overridden = 0; char* model_err = maybe_model_override_begin(body, &old_cfg, &overridden); if (model_err) { cJSON_Delete(body); reply_error(c, 400, model_err); return; } char* response = llm_chat(system->valuestring, user->valuestring); maybe_model_override_end(&old_cfg, overridden); if (!response) { cJSON_Delete(body); reply_error(c, 500, "llm request failed"); return; } cJSON* root = cJSON_CreateObject(); if (!root) { free(response); cJSON_Delete(body); reply_error(c, 500, "oom"); return; } cJSON_AddBoolToObject(root, "success", 1); cJSON_AddStringToObject(root, "response", response); llm_config_t cfg; if (llm_get_config(&cfg) == 0) { cJSON_AddStringToObject(root, "model_used", cfg.model); } int in_chars = (int)strlen(system->valuestring) + (int)strlen(user->valuestring); cJSON_AddNumberToObject(root, "input_tokens_estimate", estimate_tokens_from_chars(in_chars)); cJSON_AddNumberToObject(root, "output_tokens_estimate", estimate_tokens_from_chars((int)strlen(response))); free(response); cJSON_Delete(body); reply_json(c, 200, root); cJSON_Delete(root); } static void handle_prompt_run(struct mg_connection* c, const struct mg_http_message* hm) { cJSON* body = parse_body_json(hm); if (!body || !cJSON_IsObject(body)) { cJSON_Delete(body); reply_error(c, 400, "invalid JSON body"); return; } llm_config_t old_cfg; int overridden = 0; char* model_err = maybe_model_override_begin(body, &old_cfg, &overridden); if (model_err) { cJSON_Delete(body); reply_error(c, 400, model_err); return; } cJSON* result = run_prompt_with_tools(body); maybe_model_override_end(&old_cfg, overridden); cJSON_Delete(body); if (!result) { reply_error(c, 500, "failed to run prompt"); return; } reply_json(c, 200, result); cJSON_Delete(result); } static void handle_prompt_agent(struct mg_connection* c, const struct mg_http_message* hm) { cJSON* body = parse_body_json(hm); if (!body || !cJSON_IsObject(body)) { cJSON_Delete(body); reply_error(c, 400, "invalid JSON body"); return; } cJSON* message = cJSON_GetObjectItemCaseSensitive(body, "message"); if (!message || !cJSON_IsString(message) || !message->valuestring) { cJSON_Delete(body); reply_error(c, 400, "message string is required"); return; } char* slash_result = execute_slash_command_http(message->valuestring); if (slash_result) { cJSON* out = cJSON_CreateObject(); cJSON* turns = cJSON_CreateArray(); if (!out || !turns) { free(slash_result); cJSON_Delete(out); cJSON_Delete(turns); cJSON_Delete(body); reply_error(c, 500, "oom"); return; } cJSON_AddBoolToObject(out, "success", 1); cJSON_AddStringToObject(out, "final_response", slash_result); cJSON_AddItemToObject(out, "turns", turns); cJSON_AddNumberToObject(out, "total_input_tokens_estimate", estimate_tokens_from_chars((int)strlen(message->valuestring))); cJSON_AddNumberToObject(out, "total_output_tokens_estimate", estimate_tokens_from_chars((int)strlen(slash_result))); free(slash_result); cJSON_Delete(body); reply_json(c, 200, out); cJSON_Delete(out); return; } int max_turns = 4; if (g_api_ctx.cfg && g_api_ctx.cfg->tools.api_default_max_turns > 0) { max_turns = g_api_ctx.cfg->tools.api_default_max_turns; } cJSON* max_turns_node = cJSON_GetObjectItemCaseSensitive(body, "max_turns"); if (max_turns_node && cJSON_IsNumber(max_turns_node)) { max_turns = (int)max_turns_node->valuedouble; } llm_config_t old_cfg; int overridden = 0; char* model_err = maybe_model_override_begin(body, &old_cfg, &overridden); if (model_err) { cJSON_Delete(body); reply_error(c, 400, model_err); return; } char* base_messages_json = NULL; cJSON* convo = NULL; cJSON* result = NULL; if (agent_build_admin_messages_json(message->valuestring, DIDACTYL_SENDER_ADMIN, &base_messages_json) != 0 || !base_messages_json) { maybe_model_override_end(&old_cfg, overridden); cJSON_Delete(body); reply_error(c, 500, "failed to build agent context"); return; } convo = cJSON_Parse(base_messages_json); free(base_messages_json); if (!convo || !cJSON_IsArray(convo)) { cJSON_Delete(convo); maybe_model_override_end(&old_cfg, overridden); cJSON_Delete(body); reply_error(c, 500, "failed to initialize conversation messages"); return; } if (append_simple_message_local(convo, "user", message->valuestring) != 0) { cJSON_Delete(convo); maybe_model_override_end(&old_cfg, overridden); cJSON_Delete(body); reply_error(c, 500, "failed to append user message"); return; } cJSON* live_user_msg = cJSON_GetArrayItem(convo, cJSON_GetArraySize(convo) - 1); if (live_user_msg && cJSON_IsObject(live_user_msg)) { cJSON_AddNumberToObject(live_user_msg, "_ts", (double)time(NULL)); } result = run_prompt_with_tools_convo(convo, max_turns, "http_api_agent", "llm_chat_with_tools_messages_agent_api", "I hit my tool-use limit for this request."); cJSON_Delete(convo); maybe_model_override_end(&old_cfg, overridden); cJSON_Delete(body); if (!result) { reply_error(c, 500, "failed to run prompt"); return; } reply_json(c, 200, result); cJSON_Delete(result); } static cJSON* run_variant(cJSON* variant) { if (!variant || !cJSON_IsObject(variant)) return NULL; cJSON* body_copy = cJSON_Duplicate(variant, 1); if (!body_copy) return NULL; llm_config_t old_cfg; int overridden = 0; char* model_err = maybe_model_override_begin(body_copy, &old_cfg, &overridden); if (model_err) { cJSON* err = cJSON_CreateObject(); if (err) { cJSON_AddBoolToObject(err, "success", 0); cJSON_AddStringToObject(err, "error", model_err); } cJSON_Delete(body_copy); return err; } cJSON* out = run_prompt_with_tools(body_copy); maybe_model_override_end(&old_cfg, overridden); cJSON_Delete(body_copy); return out; } static void handle_prompt_compare(struct mg_connection* c, const struct mg_http_message* hm) { cJSON* body = parse_body_json(hm); if (!body || !cJSON_IsObject(body)) { cJSON_Delete(body); reply_error(c, 400, "invalid JSON body"); return; } cJSON* a = cJSON_GetObjectItemCaseSensitive(body, "variant_a"); cJSON* b = cJSON_GetObjectItemCaseSensitive(body, "variant_b"); if (!a || !b || !cJSON_IsObject(a) || !cJSON_IsObject(b)) { cJSON_Delete(body); reply_error(c, 400, "variant_a and variant_b objects are required"); return; } cJSON* out_a = run_variant(a); cJSON* out_b = run_variant(b); if (!out_a || !out_b) { cJSON_Delete(out_a); cJSON_Delete(out_b); cJSON_Delete(body); reply_error(c, 500, "failed to run one or more variants"); return; } cJSON* root = cJSON_CreateObject(); if (!root) { cJSON_Delete(out_a); cJSON_Delete(out_b); cJSON_Delete(body); reply_error(c, 500, "oom"); return; } cJSON_AddBoolToObject(root, "success", 1); cJSON_AddItemToObject(root, "variant_a", out_a); cJSON_AddItemToObject(root, "variant_b", out_b); cJSON_Delete(body); reply_json(c, 200, root); cJSON_Delete(root); } static void http_handler(struct mg_connection* c, int ev, void* ev_data) { if (ev == MG_EV_ACCEPT) { if (g_api_tls_enabled) { struct mg_tls_opts opts; memset(&opts, 0, sizeof(opts)); opts.cert = g_tls_cert; opts.key = g_tls_key; mg_tls_init(c, &opts); } return; } if (ev != MG_EV_HTTP_MSG) { return; } struct mg_http_message* hm = (struct mg_http_message*)ev_data; if (method_is(hm, "OPTIONS")) { mg_http_reply(c, 204, k_json_headers, ""); return; } if (method_is(hm, "GET") && mg_match(hm->uri, mg_str("/api/status"), NULL)) { handle_status(c); return; } if (method_is(hm, "GET") && mg_match(hm->uri, mg_str("/api/context/current"), NULL)) { handle_context_current(c); return; } if (method_is(hm, "GET") && mg_match(hm->uri, mg_str("/api/context/parts"), NULL)) { handle_context_parts(c); return; } if (method_is(hm, "POST") && mg_match(hm->uri, mg_str("/api/prompt/run-simple"), NULL)) { handle_prompt_run_simple(c, hm); return; } if (method_is(hm, "POST") && mg_match(hm->uri, mg_str("/api/prompt/run"), NULL)) { handle_prompt_run(c, hm); return; } if (method_is(hm, "POST") && mg_match(hm->uri, mg_str("/api/prompt/agent"), NULL)) { handle_prompt_agent(c, hm); return; } if (method_is(hm, "POST") && mg_match(hm->uri, mg_str("/api/prompt/compare"), NULL)) { handle_prompt_compare(c, hm); return; } if (method_is(hm, "POST") && mg_match(hm->uri, mg_str("/api/trigger/*"), NULL)) { char d_tag[TRIGGER_SKILL_D_TAG_MAX] = {0}; if (uri_extract_after_prefix(&hm->uri, "/api/trigger/", d_tag, sizeof(d_tag)) != 0) { reply_error(c, 400, "missing or invalid d_tag"); return; } handle_trigger_webhook(c, hm, d_tag); return; } reply_error(c, 404, "not found"); } int http_api_init(const http_api_context_t* ctx) { if (!ctx || !ctx->cfg || !ctx->tools_ctx) { return -1; } g_api_ctx = *ctx; mg_mgr_init(&g_mgr); g_api_tls_enabled = 0; g_tls_cert = mg_file_read(&mg_fs_posix, "/home/teknari/.ssl_for_local_servers/cert.pem"); g_tls_key = mg_file_read(&mg_fs_posix, "/home/teknari/.ssl_for_local_servers/key.pem"); if (g_tls_cert.buf && g_tls_cert.len > 0 && g_tls_key.buf && g_tls_key.len > 0) { g_api_tls_enabled = 1; } else { if (g_tls_cert.buf) mg_free((void*)g_tls_cert.buf); if (g_tls_key.buf) mg_free((void*)g_tls_key.buf); g_tls_cert.buf = NULL; g_tls_cert.len = 0; g_tls_key.buf = NULL; g_tls_key.len = 0; } char listen_url[320]; snprintf(listen_url, sizeof(listen_url), "%s://%s:%d", g_api_tls_enabled ? "https" : "http", g_api_ctx.cfg->api.bind_address[0] ? g_api_ctx.cfg->api.bind_address : "127.0.0.1", g_api_ctx.cfg->api.port > 0 ? g_api_ctx.cfg->api.port : 8484); g_listener = mg_http_listen(&g_mgr, listen_url, http_handler, NULL); if (!g_listener) { mg_mgr_free(&g_mgr); memset(&g_mgr, 0, sizeof(g_mgr)); return -1; } g_api_initialized = 1; DEBUG_INFO("[didactyl] http api listening on %s", listen_url); return 0; } int http_api_poll(int timeout_ms) { if (!g_api_initialized) { return 0; } if (timeout_ms < 0) timeout_ms = 0; mg_mgr_poll(&g_mgr, timeout_ms); return 0; } void http_api_cleanup(void) { if (!g_api_initialized) { return; } mg_mgr_free(&g_mgr); memset(&g_mgr, 0, sizeof(g_mgr)); g_listener = NULL; if (g_tls_cert.buf) mg_free((void*)g_tls_cert.buf); if (g_tls_key.buf) mg_free((void*)g_tls_key.buf); g_tls_cert.buf = NULL; g_tls_cert.len = 0; g_tls_key.buf = NULL; g_tls_key.len = 0; g_api_tls_enabled = 0; memset(&g_api_ctx, 0, sizeof(g_api_ctx)); g_api_initialized = 0; }