#define _POSIX_C_SOURCE 200809L #include "tools.h" #include #include #include #include #include "cjson/cJSON.h" #include "nostr_handler.h" static char* json_error(const char* msg) { cJSON* root = cJSON_CreateObject(); if (!root) return NULL; cJSON_AddBoolToObject(root, "success", 0); cJSON_AddStringToObject(root, "error", msg ? msg : "unknown error"); char* out = cJSON_PrintUnformatted(root); cJSON_Delete(root); return out; } static char* json_success_with_message(const char* msg) { cJSON* root = cJSON_CreateObject(); if (!root) return NULL; cJSON_AddBoolToObject(root, "success", 1); cJSON_AddStringToObject(root, "message", msg ? msg : "ok"); char* out = cJSON_PrintUnformatted(root); cJSON_Delete(root); return out; } static int is_safe_relative_path(const char* path) { if (!path || path[0] == '\0') return 0; if (path[0] == '/') return 0; if (strstr(path, "..") != NULL) return 0; if (strchr(path, '\\') != NULL) return 0; return 1; } static int build_tool_path(tools_context_t* ctx, const char* rel_path, char* out, size_t out_size) { if (!ctx || !ctx->cfg || !rel_path || !out || out_size == 0) return -1; if (!is_safe_relative_path(rel_path)) return -1; const char* cwd = ctx->cfg->tools.shell.working_directory[0] != '\0' ? ctx->cfg->tools.shell.working_directory : "."; int n = 0; if (strcmp(cwd, ".") == 0) { n = snprintf(out, out_size, "%s", rel_path); } else { n = snprintf(out, out_size, "%s/%s", cwd, rel_path); } if (n < 0 || (size_t)n >= out_size) return -1; return 0; } int tools_init(tools_context_t* ctx, didactyl_config_t* cfg) { if (!ctx || !cfg) return -1; memset(ctx, 0, sizeof(*ctx)); ctx->cfg = cfg; return 0; } void tools_cleanup(tools_context_t* ctx) { if (!ctx) return; memset(ctx, 0, sizeof(*ctx)); } char* tools_build_openai_schema_json(const tools_context_t* ctx) { (void)ctx; cJSON* tools = cJSON_CreateArray(); if (!tools) return NULL; cJSON* t1 = cJSON_CreateObject(); cJSON* t1_fn = cJSON_CreateObject(); cJSON* t1_params = cJSON_CreateObject(); cJSON* t1_props = cJSON_CreateObject(); cJSON* t1_required = cJSON_CreateArray(); cJSON_AddStringToObject(t1, "type", "function"); cJSON_AddStringToObject(t1_fn, "name", "nostr_post"); cJSON_AddStringToObject(t1_fn, "description", "Publish a Nostr event to connected relays"); cJSON_AddStringToObject(t1_params, "type", "object"); cJSON_AddItemToObject(t1_params, "properties", t1_props); cJSON_AddItemToObject(t1_params, "required", t1_required); cJSON* p_kind = cJSON_CreateObject(); cJSON_AddStringToObject(p_kind, "type", "integer"); cJSON_AddItemToObject(t1_props, "kind", p_kind); cJSON* p_content = cJSON_CreateObject(); cJSON_AddStringToObject(p_content, "type", "string"); cJSON_AddItemToObject(t1_props, "content", p_content); cJSON_AddItemToArray(t1_required, cJSON_CreateString("kind")); cJSON_AddItemToArray(t1_required, cJSON_CreateString("content")); cJSON_AddItemToObject(t1_fn, "parameters", t1_params); cJSON_AddItemToObject(t1, "function", t1_fn); cJSON_AddItemToArray(tools, t1); cJSON* t2 = cJSON_CreateObject(); cJSON* t2_fn = cJSON_CreateObject(); cJSON* t2_params = cJSON_CreateObject(); cJSON* t2_props = cJSON_CreateObject(); cJSON* t2_required = cJSON_CreateArray(); cJSON_AddStringToObject(t2, "type", "function"); cJSON_AddStringToObject(t2_fn, "name", "nostr_query"); cJSON_AddStringToObject(t2_fn, "description", "Query events from relays using a Nostr filter"); cJSON_AddStringToObject(t2_params, "type", "object"); cJSON_AddItemToObject(t2_params, "properties", t2_props); cJSON_AddItemToObject(t2_params, "required", t2_required); cJSON* p_filter = cJSON_CreateObject(); cJSON_AddStringToObject(p_filter, "type", "object"); cJSON_AddItemToObject(t2_props, "filter", p_filter); cJSON* p_timeout = cJSON_CreateObject(); cJSON_AddStringToObject(p_timeout, "type", "integer"); cJSON_AddItemToObject(t2_props, "timeout_ms", p_timeout); cJSON_AddItemToArray(t2_required, cJSON_CreateString("filter")); cJSON_AddItemToObject(t2_fn, "parameters", t2_params); cJSON_AddItemToObject(t2, "function", t2_fn); cJSON_AddItemToArray(tools, t2); cJSON* t3 = cJSON_CreateObject(); cJSON* t3_fn = cJSON_CreateObject(); cJSON* t3_params = cJSON_CreateObject(); cJSON* t3_props = cJSON_CreateObject(); cJSON* t3_required = cJSON_CreateArray(); cJSON_AddStringToObject(t3, "type", "function"); cJSON_AddStringToObject(t3_fn, "name", "shell_exec"); cJSON_AddStringToObject(t3_fn, "description", "Execute a shell command and return stdout/stderr"); cJSON_AddStringToObject(t3_params, "type", "object"); cJSON_AddItemToObject(t3_params, "properties", t3_props); cJSON_AddItemToObject(t3_params, "required", t3_required); cJSON* p_cmd = cJSON_CreateObject(); cJSON_AddStringToObject(p_cmd, "type", "string"); cJSON_AddItemToObject(t3_props, "command", p_cmd); cJSON_AddItemToArray(t3_required, cJSON_CreateString("command")); cJSON_AddItemToObject(t3_fn, "parameters", t3_params); cJSON_AddItemToObject(t3, "function", t3_fn); cJSON_AddItemToArray(tools, t3); cJSON* t4 = cJSON_CreateObject(); cJSON* t4_fn = cJSON_CreateObject(); cJSON* t4_params = cJSON_CreateObject(); cJSON* t4_props = cJSON_CreateObject(); cJSON* t4_required = cJSON_CreateArray(); cJSON_AddStringToObject(t4, "type", "function"); cJSON_AddStringToObject(t4_fn, "name", "file_read"); cJSON_AddStringToObject(t4_fn, "description", "Read a local file as text from the configured working directory"); cJSON_AddStringToObject(t4_params, "type", "object"); cJSON_AddItemToObject(t4_params, "properties", t4_props); cJSON_AddItemToObject(t4_params, "required", t4_required); cJSON* p_fr_path = cJSON_CreateObject(); cJSON_AddStringToObject(p_fr_path, "type", "string"); cJSON_AddItemToObject(t4_props, "path", p_fr_path); cJSON* p_fr_max = cJSON_CreateObject(); cJSON_AddStringToObject(p_fr_max, "type", "integer"); cJSON_AddItemToObject(t4_props, "max_bytes", p_fr_max); cJSON_AddItemToArray(t4_required, cJSON_CreateString("path")); cJSON_AddItemToObject(t4_fn, "parameters", t4_params); cJSON_AddItemToObject(t4, "function", t4_fn); cJSON_AddItemToArray(tools, t4); cJSON* t5 = cJSON_CreateObject(); cJSON* t5_fn = cJSON_CreateObject(); cJSON* t5_params = cJSON_CreateObject(); cJSON* t5_props = cJSON_CreateObject(); cJSON* t5_required = cJSON_CreateArray(); cJSON_AddStringToObject(t5, "type", "function"); cJSON_AddStringToObject(t5_fn, "name", "file_write"); cJSON_AddStringToObject(t5_fn, "description", "Write text content to a local file in the configured working directory"); cJSON_AddStringToObject(t5_params, "type", "object"); cJSON_AddItemToObject(t5_params, "properties", t5_props); cJSON_AddItemToObject(t5_params, "required", t5_required); cJSON* p_fw_path = cJSON_CreateObject(); cJSON_AddStringToObject(p_fw_path, "type", "string"); cJSON_AddItemToObject(t5_props, "path", p_fw_path); cJSON* p_fw_content = cJSON_CreateObject(); cJSON_AddStringToObject(p_fw_content, "type", "string"); cJSON_AddItemToObject(t5_props, "content", p_fw_content); cJSON* p_fw_append = cJSON_CreateObject(); cJSON_AddStringToObject(p_fw_append, "type", "boolean"); cJSON_AddItemToObject(t5_props, "append", p_fw_append); cJSON_AddItemToArray(t5_required, cJSON_CreateString("path")); cJSON_AddItemToArray(t5_required, cJSON_CreateString("content")); cJSON_AddItemToObject(t5_fn, "parameters", t5_params); cJSON_AddItemToObject(t5, "function", t5_fn); cJSON_AddItemToArray(tools, t5); char* out = cJSON_PrintUnformatted(tools); cJSON_Delete(tools); return out; } static char* execute_nostr_post(const char* args_json) { cJSON* args = cJSON_Parse(args_json ? args_json : "{}"); if (!args) return json_error("invalid arguments JSON"); cJSON* kind = cJSON_GetObjectItemCaseSensitive(args, "kind"); cJSON* content = cJSON_GetObjectItemCaseSensitive(args, "content"); if (!kind || !cJSON_IsNumber(kind) || !content || !cJSON_IsString(content) || !content->valuestring) { cJSON_Delete(args); return json_error("nostr_post requires integer kind and string content"); } int rc = nostr_handler_publish_kind_event((int)kind->valuedouble, content->valuestring, NULL); cJSON_Delete(args); if (rc != 0) return json_error("nostr_post failed"); return json_success_with_message("nostr_post published"); } static char* execute_nostr_query(const char* args_json) { cJSON* args = cJSON_Parse(args_json ? args_json : "{}"); if (!args) return json_error("invalid arguments JSON"); cJSON* filter = cJSON_GetObjectItemCaseSensitive(args, "filter"); cJSON* timeout = cJSON_GetObjectItemCaseSensitive(args, "timeout_ms"); if (!filter || !cJSON_IsObject(filter)) { cJSON_Delete(args); return json_error("nostr_query requires object filter"); } cJSON* filter_dup = cJSON_Duplicate(filter, 1); if (!filter_dup) { cJSON_Delete(args); return json_error("failed to duplicate filter"); } int timeout_ms = (timeout && cJSON_IsNumber(timeout)) ? (int)timeout->valuedouble : 8000; char* events_json = nostr_handler_query_json(filter_dup, timeout_ms); cJSON_Delete(filter_dup); cJSON_Delete(args); if (!events_json) return json_error("nostr_query failed"); cJSON* out = cJSON_CreateObject(); if (!out) { free(events_json); return NULL; } cJSON* events = cJSON_Parse(events_json); free(events_json); if (!events) { cJSON_Delete(out); return json_error("nostr_query returned invalid JSON"); } cJSON_AddBoolToObject(out, "success", 1); cJSON_AddItemToObject(out, "events", events); char* json = cJSON_PrintUnformatted(out); cJSON_Delete(out); return json; } static char* execute_shell_exec(tools_context_t* ctx, const char* args_json) { if (!ctx || !ctx->cfg) return json_error("tool context unavailable"); if (!ctx->cfg->tools.shell.enabled) return json_error("shell tool disabled"); cJSON* args = cJSON_Parse(args_json ? args_json : "{}"); if (!args) return json_error("invalid arguments JSON"); cJSON* command = cJSON_GetObjectItemCaseSensitive(args, "command"); if (!command || !cJSON_IsString(command) || !command->valuestring) { cJSON_Delete(args); return json_error("shell_exec requires string command"); } const char* cwd = ctx->cfg->tools.shell.working_directory[0] != '\0' ? ctx->cfg->tools.shell.working_directory : "."; int timeout_s = ctx->cfg->tools.shell.timeout_seconds > 0 ? ctx->cfg->tools.shell.timeout_seconds : 30; char cmd[4096]; snprintf(cmd, sizeof(cmd), "cd %s && timeout %ds sh -lc %s 2>&1", cwd, timeout_s, command->valuestring); FILE* fp = popen(cmd, "r"); cJSON_Delete(args); if (!fp) return json_error("failed to execute command"); int max_bytes = ctx->cfg->tools.shell.max_output_bytes > 0 ? ctx->cfg->tools.shell.max_output_bytes : 65536; char* output = (char*)calloc((size_t)max_bytes + 1U, 1U); if (!output) { pclose(fp); return json_error("allocation failure"); } size_t used = 0; while (!feof(fp) && used < (size_t)max_bytes) { size_t n = fread(output + used, 1, (size_t)max_bytes - used, fp); used += n; if (n == 0) break; } int status = pclose(fp); cJSON* out = cJSON_CreateObject(); if (!out) { free(output); return NULL; } cJSON_AddBoolToObject(out, "success", status == 0 ? 1 : 0); cJSON_AddNumberToObject(out, "exit_status", status); cJSON_AddStringToObject(out, "output", output); free(output); char* json = cJSON_PrintUnformatted(out); cJSON_Delete(out); return json; } static char* execute_file_read(tools_context_t* ctx, const char* args_json) { if (!ctx || !ctx->cfg) return json_error("tool context unavailable"); cJSON* args = cJSON_Parse(args_json ? args_json : "{}"); if (!args) return json_error("invalid arguments JSON"); cJSON* path = cJSON_GetObjectItemCaseSensitive(args, "path"); cJSON* maxb = cJSON_GetObjectItemCaseSensitive(args, "max_bytes"); if (!path || !cJSON_IsString(path) || !path->valuestring) { cJSON_Delete(args); return json_error("file_read requires string path"); } int hard_max = ctx->cfg->tools.shell.max_output_bytes > 0 ? ctx->cfg->tools.shell.max_output_bytes : 65536; int max_bytes = (maxb && cJSON_IsNumber(maxb)) ? (int)maxb->valuedouble : hard_max; if (max_bytes <= 0 || max_bytes > hard_max) max_bytes = hard_max; char file_path[PATH_MAX]; if (build_tool_path(ctx, path->valuestring, file_path, sizeof(file_path)) != 0) { cJSON_Delete(args); return json_error("file_read path is not allowed"); } FILE* fp = fopen(file_path, "rb"); cJSON_Delete(args); if (!fp) return json_error("file_read failed to open file"); char* buf = (char*)calloc((size_t)max_bytes + 1U, 1U); if (!buf) { fclose(fp); return json_error("allocation failure"); } size_t n = fread(buf, 1, (size_t)max_bytes, fp); int truncated = !feof(fp) ? 1 : 0; fclose(fp); buf[n] = '\0'; cJSON* out = cJSON_CreateObject(); if (!out) { free(buf); return NULL; } cJSON_AddBoolToObject(out, "success", 1); cJSON_AddStringToObject(out, "path", file_path); cJSON_AddNumberToObject(out, "bytes_read", (double)n); cJSON_AddBoolToObject(out, "truncated", truncated); cJSON_AddStringToObject(out, "content", buf); free(buf); char* json = cJSON_PrintUnformatted(out); cJSON_Delete(out); return json; } static char* execute_file_write(tools_context_t* ctx, const char* args_json) { if (!ctx || !ctx->cfg) return json_error("tool context unavailable"); cJSON* args = cJSON_Parse(args_json ? args_json : "{}"); if (!args) return json_error("invalid arguments JSON"); cJSON* path = cJSON_GetObjectItemCaseSensitive(args, "path"); cJSON* content = cJSON_GetObjectItemCaseSensitive(args, "content"); cJSON* append = cJSON_GetObjectItemCaseSensitive(args, "append"); if (!path || !cJSON_IsString(path) || !path->valuestring || !content || !cJSON_IsString(content) || !content->valuestring) { cJSON_Delete(args); return json_error("file_write requires string path and content"); } char file_path[PATH_MAX]; if (build_tool_path(ctx, path->valuestring, file_path, sizeof(file_path)) != 0) { cJSON_Delete(args); return json_error("file_write path is not allowed"); } const char* content_str = content->valuestring; size_t len = strlen(content_str); int do_append = (append && cJSON_IsBool(append) && cJSON_IsTrue(append)) ? 1 : 0; FILE* fp = fopen(file_path, do_append ? "ab" : "wb"); cJSON_Delete(args); if (!fp) return json_error("file_write failed to open file"); size_t n = fwrite(content_str, 1, len, fp); fclose(fp); if (n != len) return json_error("file_write failed to write all bytes"); cJSON* out = cJSON_CreateObject(); if (!out) return NULL; cJSON_AddBoolToObject(out, "success", 1); cJSON_AddStringToObject(out, "path", file_path); cJSON_AddNumberToObject(out, "bytes_written", (double)n); cJSON_AddBoolToObject(out, "append", do_append); char* json = cJSON_PrintUnformatted(out); cJSON_Delete(out); return json; } char* tools_execute(tools_context_t* ctx, const char* tool_name, const char* args_json) { if (!tool_name) return json_error("missing tool name"); if (strcmp(tool_name, "nostr_post") == 0) { return execute_nostr_post(args_json); } if (strcmp(tool_name, "nostr_query") == 0) { return execute_nostr_query(args_json); } if (strcmp(tool_name, "shell_exec") == 0) { return execute_shell_exec(ctx, args_json); } if (strcmp(tool_name, "file_read") == 0) { return execute_file_read(ctx, args_json); } if (strcmp(tool_name, "file_write") == 0) { return execute_file_write(ctx, args_json); } return json_error("unknown tool"); }