Files
didactyl/src/tools.c
T

603 lines
21 KiB
C

#define _POSIX_C_SOURCE 200809L
#include "tools.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <limits.h>
#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 char* sanitize_json_string_controls(const char* in) {
if (!in) return NULL;
size_t len = strlen(in);
size_t cap = (len * 2U) + 1U;
char* out = (char*)malloc(cap);
if (!out) return NULL;
int in_string = 0;
int escaping = 0;
size_t j = 0;
for (size_t i = 0; i < len; i++) {
char c = in[i];
if (escaping) {
if (j + 1U >= cap) {
free(out);
return NULL;
}
out[j++] = c;
escaping = 0;
continue;
}
if (c == '\\') {
if (j + 1U >= cap) {
free(out);
return NULL;
}
out[j++] = c;
if (in_string) escaping = 1;
continue;
}
if (c == '"') {
if (j + 1U >= cap) {
free(out);
return NULL;
}
out[j++] = c;
in_string = !in_string;
continue;
}
if (in_string && (c == '\n' || c == '\r' || c == '\t')) {
if (j + 2U >= cap) {
free(out);
return NULL;
}
out[j++] = '\\';
out[j++] = (c == '\n') ? 'n' : (c == '\r') ? 'r' : 't';
continue;
}
if (j + 1U >= cap) {
free(out);
return NULL;
}
out[j++] = c;
}
out[j] = '\0';
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* p_tags = cJSON_CreateObject();
cJSON_AddStringToObject(p_tags, "type", "array");
cJSON_AddStringToObject(p_tags, "description", "Optional Nostr tags array, e.g. [[\"d\",\"slug\"],[\"t\",\"nostr\"]]");
cJSON* p_tags_items = cJSON_CreateObject();
cJSON_AddStringToObject(p_tags_items, "type", "array");
cJSON* p_tag_item = cJSON_CreateObject();
cJSON_AddStringToObject(p_tag_item, "type", "string");
cJSON_AddItemToObject(p_tags_items, "items", p_tag_item);
cJSON_AddItemToObject(p_tags, "items", p_tags_items);
cJSON_AddItemToObject(t1_props, "tags", p_tags);
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 : "{}");
char* repaired_args_json = NULL;
if (!args && args_json) {
repaired_args_json = sanitize_json_string_controls(args_json);
if (repaired_args_json) {
args = cJSON_Parse(repaired_args_json);
}
}
if (!args) {
free(repaired_args_json);
return json_error("invalid arguments JSON");
}
cJSON* kind = cJSON_GetObjectItemCaseSensitive(args, "kind");
cJSON* content = cJSON_GetObjectItemCaseSensitive(args, "content");
cJSON* tags = cJSON_GetObjectItemCaseSensitive(args, "tags");
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");
}
if (tags && !cJSON_IsArray(tags)) {
cJSON_Delete(args);
return json_error("nostr_post tags must be an array when provided");
}
cJSON* tags_dup = NULL;
if (tags) {
tags_dup = cJSON_Duplicate(tags, 1);
if (!tags_dup) {
cJSON_Delete(args);
return json_error("nostr_post failed to duplicate tags");
}
}
nostr_publish_result_t publish_result;
memset(&publish_result, 0, sizeof(publish_result));
int rc = nostr_handler_publish_kind_event((int)kind->valuedouble,
content->valuestring,
tags_dup,
&publish_result);
cJSON_Delete(tags_dup);
cJSON_Delete(args);
free(repaired_args_json);
if (rc != 0) {
nostr_handler_publish_result_free(&publish_result);
return json_error("nostr_post failed");
}
cJSON* out = cJSON_CreateObject();
if (!out) {
nostr_handler_publish_result_free(&publish_result);
return NULL;
}
cJSON_AddBoolToObject(out, "success", publish_result.success ? 1 : 0);
cJSON_AddStringToObject(out, "message", "nostr_post published");
cJSON_AddNumberToObject(out, "kind", publish_result.kind);
cJSON_AddStringToObject(out, "event_id", publish_result.event_id);
cJSON_AddNumberToObject(out, "relay_count", publish_result.relay_count);
cJSON_AddNumberToObject(out, "accepted_by_pool_count", publish_result.accepted_by_pool_count);
if (publish_result.note_uri[0] != '\0') {
cJSON_AddStringToObject(out, "note_uri", publish_result.note_uri);
}
if (publish_result.naddr_uri[0] != '\0') {
cJSON_AddStringToObject(out, "naddr_uri", publish_result.naddr_uri);
}
if (publish_result.d_tag[0] != '\0') {
cJSON_AddStringToObject(out, "d_tag", publish_result.d_tag);
}
cJSON* relays = cJSON_CreateArray();
if (!relays) {
nostr_handler_publish_result_free(&publish_result);
cJSON_Delete(out);
return NULL;
}
for (int i = 0; i < publish_result.relay_count; i++) {
cJSON_AddItemToArray(relays, cJSON_CreateString(publish_result.relays[i] ? publish_result.relays[i] : ""));
}
cJSON_AddItemToObject(out, "relays", relays);
char* json = cJSON_PrintUnformatted(out);
cJSON_Delete(out);
nostr_handler_publish_result_free(&publish_result);
return json;
}
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");
}