441 lines
13 KiB
C
441 lines
13 KiB
C
#define _POSIX_C_SOURCE 200809L
|
|
|
|
#include "tools_internal.h"
|
|
|
|
#include <pthread.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#include "cjson/cJSON.h"
|
|
#include "../nostr_handler.h"
|
|
#include "../../nostr_core_lib/nostr_core/nostr_core.h"
|
|
|
|
#define MEMORY_KIND 30078
|
|
#define MEMORY_D_TAG "memory"
|
|
#define MEMORY_APP_TAG "didactyl"
|
|
#define MEMORY_MAX_CHARS 32000
|
|
|
|
static pthread_mutex_t g_memory_mutex = PTHREAD_MUTEX_INITIALIZER;
|
|
static char* g_memory_plain = NULL;
|
|
static int g_memory_loaded = 0;
|
|
|
|
static char* json_error_local(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 cJSON* parse_args_local(const char* args_json) {
|
|
const char* raw = args_json ? args_json : "{}";
|
|
cJSON* args = cJSON_Parse(raw);
|
|
if (!args || !cJSON_IsObject(args)) {
|
|
cJSON_Delete(args);
|
|
return NULL;
|
|
}
|
|
return args;
|
|
}
|
|
|
|
static char* strdup_nonnull_local(const char* s) {
|
|
return strdup(s ? s : "");
|
|
}
|
|
|
|
static void memory_cache_set_locked_local(const char* plaintext) {
|
|
free(g_memory_plain);
|
|
g_memory_plain = strdup_nonnull_local(plaintext);
|
|
g_memory_loaded = 1;
|
|
}
|
|
|
|
static char* memory_cache_get_copy_locked_local(void) {
|
|
return strdup(g_memory_plain ? g_memory_plain : "");
|
|
}
|
|
|
|
static int nip44_encrypt_self_local(tools_context_t* ctx, const char* plaintext, char** out_ciphertext) {
|
|
if (!ctx || !ctx->cfg || !out_ciphertext) return -1;
|
|
|
|
const char* plain = plaintext ? plaintext : "";
|
|
|
|
/* Phase 3 item 13: route self-encrypt through the process signer when
|
|
* wired into the tools context so remote signer modes keep the nsec out
|
|
* of the agent. Falls back to the legacy raw-key path otherwise. */
|
|
if (ctx->signer) {
|
|
char* signer_out = NULL;
|
|
int rc = nostr_signer_nip44_encrypt(ctx->signer,
|
|
ctx->cfg->keys.public_key_hex,
|
|
plain,
|
|
&signer_out);
|
|
if (rc != NOSTR_SUCCESS || !signer_out) return -1;
|
|
*out_ciphertext = signer_out;
|
|
return 0;
|
|
}
|
|
|
|
size_t out_cap = (strlen(plain) * 4U) + 1024U;
|
|
char* ciphertext = (char*)malloc(out_cap);
|
|
if (!ciphertext) return -1;
|
|
|
|
int rc = nostr_nip44_encrypt(ctx->cfg->keys.private_key,
|
|
ctx->cfg->keys.public_key,
|
|
plain,
|
|
ciphertext,
|
|
out_cap);
|
|
if (rc != NOSTR_SUCCESS) {
|
|
free(ciphertext);
|
|
return -1;
|
|
}
|
|
|
|
*out_ciphertext = ciphertext;
|
|
return 0;
|
|
}
|
|
|
|
static int nip44_decrypt_self_local(tools_context_t* ctx, const char* ciphertext, char** out_plaintext) {
|
|
if (!ctx || !ctx->cfg || !ciphertext || !out_plaintext) return -1;
|
|
|
|
/* Phase 3 item 13: route self-decrypt through the process signer when
|
|
* wired into the tools context. Falls back to the legacy raw-key path. */
|
|
if (ctx->signer) {
|
|
char* signer_out = NULL;
|
|
int rc = nostr_signer_nip44_decrypt(ctx->signer,
|
|
ctx->cfg->keys.public_key_hex,
|
|
ciphertext,
|
|
&signer_out);
|
|
if (rc != NOSTR_SUCCESS || !signer_out) return -1;
|
|
*out_plaintext = signer_out;
|
|
return 0;
|
|
}
|
|
|
|
size_t out_cap = strlen(ciphertext) + 1024U;
|
|
char* plaintext = (char*)malloc(out_cap);
|
|
if (!plaintext) return -1;
|
|
|
|
int rc = nostr_nip44_decrypt(ctx->cfg->keys.private_key,
|
|
ctx->cfg->keys.public_key,
|
|
ciphertext,
|
|
plaintext,
|
|
out_cap);
|
|
if (rc != NOSTR_SUCCESS) {
|
|
free(plaintext);
|
|
return -1;
|
|
}
|
|
|
|
*out_plaintext = plaintext;
|
|
return 0;
|
|
}
|
|
|
|
static int query_memory_ciphertext_local(tools_context_t* ctx, char** out_ciphertext) {
|
|
if (!ctx || !ctx->cfg || !out_ciphertext) return -1;
|
|
|
|
cJSON* filter = cJSON_CreateObject();
|
|
cJSON* kinds = cJSON_CreateArray();
|
|
cJSON* authors = cJSON_CreateArray();
|
|
cJSON* d_vals = cJSON_CreateArray();
|
|
if (!filter || !kinds || !authors || !d_vals) {
|
|
cJSON_Delete(filter);
|
|
cJSON_Delete(kinds);
|
|
cJSON_Delete(authors);
|
|
cJSON_Delete(d_vals);
|
|
return -1;
|
|
}
|
|
|
|
cJSON_AddItemToArray(kinds, cJSON_CreateNumber(MEMORY_KIND));
|
|
cJSON_AddItemToObject(filter, "kinds", kinds);
|
|
|
|
cJSON_AddItemToArray(authors, cJSON_CreateString(ctx->cfg->keys.public_key_hex));
|
|
cJSON_AddItemToObject(filter, "authors", authors);
|
|
|
|
cJSON_AddItemToArray(d_vals, cJSON_CreateString(MEMORY_D_TAG));
|
|
cJSON_AddItemToObject(filter, "#d", d_vals);
|
|
|
|
cJSON_AddNumberToObject(filter, "limit", 1);
|
|
|
|
char* events_json = nostr_handler_query_json(filter, 4000);
|
|
cJSON_Delete(filter);
|
|
if (!events_json) {
|
|
return -1;
|
|
}
|
|
|
|
cJSON* arr = cJSON_Parse(events_json);
|
|
free(events_json);
|
|
if (!arr || !cJSON_IsArray(arr) || cJSON_GetArraySize(arr) <= 0) {
|
|
cJSON_Delete(arr);
|
|
*out_ciphertext = strdup("");
|
|
return (*out_ciphertext != NULL) ? 0 : -1;
|
|
}
|
|
|
|
cJSON* ev = cJSON_GetArrayItem(arr, 0);
|
|
cJSON* content = ev ? cJSON_GetObjectItemCaseSensitive(ev, "content") : NULL;
|
|
const char* cipher = (content && cJSON_IsString(content) && content->valuestring) ? content->valuestring : "";
|
|
|
|
*out_ciphertext = strdup(cipher);
|
|
cJSON_Delete(arr);
|
|
return (*out_ciphertext != NULL) ? 0 : -1;
|
|
}
|
|
|
|
static int memory_load_local(tools_context_t* ctx) {
|
|
char* ciphertext = NULL;
|
|
if (query_memory_ciphertext_local(ctx, &ciphertext) != 0) {
|
|
return -1;
|
|
}
|
|
|
|
char* plaintext = NULL;
|
|
if (!ciphertext || ciphertext[0] == '\0') {
|
|
plaintext = strdup("");
|
|
} else if (nip44_decrypt_self_local(ctx, ciphertext, &plaintext) != 0) {
|
|
fprintf(stdout,
|
|
"[didactyl] warning: failed to NIP-44 decrypt memory; resetting to empty\n");
|
|
plaintext = strdup("");
|
|
}
|
|
free(ciphertext);
|
|
|
|
if (!plaintext) return -1;
|
|
|
|
pthread_mutex_lock(&g_memory_mutex);
|
|
memory_cache_set_locked_local(plaintext);
|
|
pthread_mutex_unlock(&g_memory_mutex);
|
|
|
|
free(plaintext);
|
|
return 0;
|
|
}
|
|
|
|
static char* trim_and_limit_entry_local(const char* in, int* out_truncated) {
|
|
if (out_truncated) *out_truncated = 0;
|
|
if (!in) return strdup("");
|
|
|
|
while (*in == ' ' || *in == '\t' || *in == '\r' || *in == '\n') in++;
|
|
size_t n = strlen(in);
|
|
while (n > 0 && (in[n - 1] == ' ' || in[n - 1] == '\t' || in[n - 1] == '\r' || in[n - 1] == '\n')) {
|
|
n--;
|
|
}
|
|
|
|
if (n > MEMORY_MAX_CHARS) {
|
|
n = MEMORY_MAX_CHARS;
|
|
if (out_truncated) *out_truncated = 1;
|
|
}
|
|
|
|
char* out = (char*)malloc(n + 1U);
|
|
if (!out) return NULL;
|
|
if (n > 0) memcpy(out, in, n);
|
|
out[n] = '\0';
|
|
return out;
|
|
}
|
|
|
|
static char* prepend_and_truncate_local(const char* existing, const char* incoming, int* out_truncated) {
|
|
if (out_truncated) *out_truncated = 0;
|
|
|
|
const char* old = existing ? existing : "";
|
|
const char* add = incoming ? incoming : "";
|
|
const char* sep = "\n\n---\n\n";
|
|
|
|
size_t add_len = strlen(add);
|
|
size_t old_len = strlen(old);
|
|
size_t sep_len = (add_len > 0 && old_len > 0) ? strlen(sep) : 0U;
|
|
|
|
size_t total_len = add_len + sep_len + old_len;
|
|
char* joined = (char*)malloc(total_len + 1U);
|
|
if (!joined) return NULL;
|
|
|
|
size_t used = 0U;
|
|
if (add_len > 0) {
|
|
memcpy(joined + used, add, add_len);
|
|
used += add_len;
|
|
}
|
|
if (sep_len > 0) {
|
|
memcpy(joined + used, sep, sep_len);
|
|
used += sep_len;
|
|
}
|
|
if (old_len > 0) {
|
|
memcpy(joined + used, old, old_len);
|
|
used += old_len;
|
|
}
|
|
joined[used] = '\0';
|
|
|
|
if (used <= MEMORY_MAX_CHARS) {
|
|
return joined;
|
|
}
|
|
|
|
if (out_truncated) *out_truncated = 1;
|
|
char* clipped = (char*)malloc(MEMORY_MAX_CHARS + 1U);
|
|
if (!clipped) {
|
|
free(joined);
|
|
return NULL;
|
|
}
|
|
memcpy(clipped, joined, MEMORY_MAX_CHARS);
|
|
clipped[MEMORY_MAX_CHARS] = '\0';
|
|
free(joined);
|
|
return clipped;
|
|
}
|
|
|
|
int memory_init(tools_context_t* ctx) {
|
|
if (!ctx || !ctx->cfg) return -1;
|
|
return memory_load_local(ctx);
|
|
}
|
|
|
|
void memory_cleanup(void) {
|
|
pthread_mutex_lock(&g_memory_mutex);
|
|
free(g_memory_plain);
|
|
g_memory_plain = NULL;
|
|
g_memory_loaded = 0;
|
|
pthread_mutex_unlock(&g_memory_mutex);
|
|
}
|
|
|
|
char* execute_memory_save(tools_context_t* ctx, const char* args_json) {
|
|
if (!ctx || !ctx->cfg) return json_error_local("tool context unavailable");
|
|
|
|
cJSON* args = parse_args_local(args_json);
|
|
if (!args) return json_error_local("invalid arguments JSON");
|
|
|
|
cJSON* content_j = cJSON_GetObjectItemCaseSensitive(args, "content");
|
|
if (!content_j || !cJSON_IsString(content_j) || !content_j->valuestring) {
|
|
cJSON_Delete(args);
|
|
return json_error_local("memory_save requires string content");
|
|
}
|
|
|
|
int entry_truncated = 0;
|
|
char* incoming = trim_and_limit_entry_local(content_j->valuestring, &entry_truncated);
|
|
cJSON_Delete(args);
|
|
if (!incoming) return NULL;
|
|
if (incoming[0] == '\0') {
|
|
free(incoming);
|
|
return json_error_local("memory_save content cannot be empty");
|
|
}
|
|
|
|
pthread_mutex_lock(&g_memory_mutex);
|
|
char* old_copy = memory_cache_get_copy_locked_local();
|
|
pthread_mutex_unlock(&g_memory_mutex);
|
|
if (!old_copy) {
|
|
free(incoming);
|
|
return NULL;
|
|
}
|
|
|
|
int body_truncated = 0;
|
|
char* merged = prepend_and_truncate_local(old_copy, incoming, &body_truncated);
|
|
free(old_copy);
|
|
free(incoming);
|
|
if (!merged) return NULL;
|
|
|
|
char* ciphertext = NULL;
|
|
if (nip44_encrypt_self_local(ctx, merged, &ciphertext) != 0) {
|
|
free(merged);
|
|
return json_error_local("memory_save NIP-44 encryption failed");
|
|
}
|
|
|
|
cJSON* tags = cJSON_CreateArray();
|
|
if (!tags) {
|
|
free(merged);
|
|
free(ciphertext);
|
|
return NULL;
|
|
}
|
|
|
|
cJSON* d_tag = cJSON_CreateArray();
|
|
cJSON* app_tag = cJSON_CreateArray();
|
|
if (!d_tag || !app_tag) {
|
|
cJSON_Delete(d_tag);
|
|
cJSON_Delete(app_tag);
|
|
cJSON_Delete(tags);
|
|
free(merged);
|
|
free(ciphertext);
|
|
return NULL;
|
|
}
|
|
|
|
cJSON_AddItemToArray(d_tag, cJSON_CreateString("d"));
|
|
cJSON_AddItemToArray(d_tag, cJSON_CreateString(MEMORY_D_TAG));
|
|
cJSON_AddItemToArray(tags, d_tag);
|
|
|
|
cJSON_AddItemToArray(app_tag, cJSON_CreateString("app"));
|
|
cJSON_AddItemToArray(app_tag, cJSON_CreateString(MEMORY_APP_TAG));
|
|
cJSON_AddItemToArray(tags, app_tag);
|
|
|
|
nostr_publish_result_t publish_result;
|
|
memset(&publish_result, 0, sizeof(publish_result));
|
|
|
|
int rc = nostr_handler_publish_kind_event(MEMORY_KIND, ciphertext, tags, &publish_result);
|
|
cJSON_Delete(tags);
|
|
free(ciphertext);
|
|
|
|
if (rc != 0) {
|
|
free(merged);
|
|
nostr_handler_publish_result_free(&publish_result);
|
|
return json_error_local("memory_save publish failed");
|
|
}
|
|
|
|
pthread_mutex_lock(&g_memory_mutex);
|
|
memory_cache_set_locked_local(merged);
|
|
pthread_mutex_unlock(&g_memory_mutex);
|
|
|
|
cJSON* out = cJSON_CreateObject();
|
|
if (!out) {
|
|
free(merged);
|
|
nostr_handler_publish_result_free(&publish_result);
|
|
return NULL;
|
|
}
|
|
|
|
cJSON_AddBoolToObject(out, "success", 1);
|
|
cJSON_AddStringToObject(out, "d_tag", MEMORY_D_TAG);
|
|
cJSON_AddNumberToObject(out, "kind", MEMORY_KIND);
|
|
cJSON_AddNumberToObject(out, "entry_length", (double)strlen(merged));
|
|
cJSON_AddBoolToObject(out, "truncated", (entry_truncated || body_truncated) ? 1 : 0);
|
|
cJSON_AddStringToObject(out, "event_id", publish_result.event_id);
|
|
|
|
char* json = cJSON_PrintUnformatted(out);
|
|
cJSON_Delete(out);
|
|
free(merged);
|
|
nostr_handler_publish_result_free(&publish_result);
|
|
return json;
|
|
}
|
|
|
|
char* execute_memory_recall(tools_context_t* ctx, const char* args_json) {
|
|
if (!ctx || !ctx->cfg) return json_error_local("tool context unavailable");
|
|
|
|
cJSON* args = parse_args_local(args_json);
|
|
if (!args) return json_error_local("invalid arguments JSON");
|
|
cJSON_Delete(args);
|
|
|
|
pthread_mutex_lock(&g_memory_mutex);
|
|
int loaded = g_memory_loaded;
|
|
pthread_mutex_unlock(&g_memory_mutex);
|
|
|
|
if (!loaded) {
|
|
(void)memory_load_local(ctx);
|
|
}
|
|
|
|
pthread_mutex_lock(&g_memory_mutex);
|
|
char* memory = memory_cache_get_copy_locked_local();
|
|
pthread_mutex_unlock(&g_memory_mutex);
|
|
if (!memory) return NULL;
|
|
|
|
cJSON* out = cJSON_CreateObject();
|
|
if (!out) {
|
|
free(memory);
|
|
return NULL;
|
|
}
|
|
|
|
cJSON_AddBoolToObject(out, "success", 1);
|
|
cJSON_AddStringToObject(out, "content", memory);
|
|
cJSON_AddNumberToObject(out, "content_length", (double)strlen(memory));
|
|
|
|
char* json = cJSON_PrintUnformatted(out);
|
|
cJSON_Delete(out);
|
|
free(memory);
|
|
return json;
|
|
}
|
|
|
|
char* execute_my_memory(tools_context_t* ctx, const char* args_json) {
|
|
return execute_memory_recall(ctx, args_json);
|
|
}
|
|
|
|
char* execute_memory_short_term_read(tools_context_t* ctx, const char* args_json) {
|
|
(void)ctx;
|
|
(void)args_json;
|
|
|
|
cJSON* out = cJSON_CreateObject();
|
|
if (!out) return NULL;
|
|
cJSON_AddBoolToObject(out, "success", 1);
|
|
cJSON_AddStringToObject(out, "content", "");
|
|
char* json = cJSON_PrintUnformatted(out);
|
|
cJSON_Delete(out);
|
|
return json;
|
|
}
|