v0.0.71 - Add nprofile, nevent, and naddr support to nostr_encode with argument validation

This commit is contained in:
Your Name
2026-03-13 15:42:28 -04:00
parent f9cb6b19c2
commit 5857058a21
10 changed files with 13883 additions and 44 deletions

View File

@@ -55,11 +55,11 @@ Skills support context modes (`inject`, `full`, `override`) and per-skill LLM fa
Didactyl will support local inference, which is very privacy preserving. Remote inference does however have it's advantages, and in those cases Didactyl supports using Bitcoin Lightning and eCash inference providers.
## Current Status — v0.0.70
## Current Status — v0.0.71
**Active build — this project is barely working. Experiment at your own risk.**
> Last release update: v0.0.70 — Add c_utils_lib category-based debug filtering with tests/docs and root API log tail shortcut script
> Last release update: v0.0.71 — Add nprofile, nevent, and naddr support to nostr_encode with argument validation
- Connects to configured relays with auto-reconnect and relay state transition logging
- Publishes configured startup events per relay as each relay becomes connected

File diff suppressed because one or more lines are too long

4194
didactyl.log Normal file

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,81 @@
# Didactyl Integration Plan: Unified `nostr_core_lib` Logging into One File
## Goal
Route `nostr_core_lib` logs through didactyl logging so there is one authoritative file for errors, using existing didactyl logging in [`debug.c`](../src/debug.c:1) plus the new callback API in [`nostr_log.h`](../nostr_core_lib/nostr_core/nostr_log.h:1).
## Current State
- Didactyl writes logs to path from `DIDACTYL_LOG_FILE` or default `debug.log` in [`debug_open_log_file`](../src/debug.c:10).
- Didactyl log emission is centralized in [`debug_log`](../src/debug.c:31).
- Relay pool setup occurs in [`nostr_handler_init`](../src/nostr_handler.c:1519).
- `nostr_core_lib` now exposes callback logging API in [`nostr_log.h`](../nostr_core_lib/nostr_core/nostr_log.h:1).
## Desired End State
- `nostr_core_lib` logs are forwarded into didactyl logger via callback registration.
- One file path configured by `DIDACTYL_LOG_FILE` receives didactyl plus nostr_core_lib errors.
- Log lines preserve source component tags such as `nostr:websocket` and `nostr:nip013`.
## Architecture
```mermaid
flowchart TD
A[nostr_core_lib emits callback log] --> B[didactyl bridge callback in nostr_handler.c]
B --> C[map nostr level to didactyl level]
C --> D[debug_log in debug.c]
D --> E[DIDACTYL_LOG_FILE single destination]
```
## Implementation Steps
1. Add bridge function in [`nostr_handler.c`](../src/nostr_handler.c:1519)
- Create static callback `nostr_core_log_bridge level component message user_data`.
- Prefix forwarded lines with `nostr:` namespace.
2. Map levels from `nostr_core_lib` to didactyl
- `NOSTR_LOG_LEVEL_ERROR` to `DEBUG_LEVEL_ERROR`
- `NOSTR_LOG_LEVEL_WARN` to `DEBUG_LEVEL_WARN`
- `NOSTR_LOG_LEVEL_INFO` to `DEBUG_LEVEL_INFO`
- `NOSTR_LOG_LEVEL_DEBUG` to `DEBUG_LEVEL_DEBUG`
- `NOSTR_LOG_LEVEL_TRACE` to `DEBUG_LEVEL_TRACE`
3. Register callback during startup in [`nostr_handler_init`](../src/nostr_handler.c:1519)
- Before relay setup, call `nostr_set_log_callback`.
- Set threshold with `nostr_set_log_level` aligned to current didactyl debug level.
4. Enforce single-file policy
- Set `DIDACTYL_LOG_FILE` to desired final log path.
- Keep didactyl as only file owner via [`debug_open_log_file`](../src/debug.c:10).
5. Ensure error visibility
- Keep bridge forwarding at least error and warn levels even when app runs at lower verbosity.
- Optionally pin library threshold to `WARN` or `ERROR` for production profiles.
## Validation Plan
1. Start didactyl with explicit file path
- Example environment: `DIDACTYL_LOG_FILE=didactyl.log`.
2. Trigger nostr websocket activity
- Confirm `nostr:websocket` entries appear in same file.
3. Trigger nip013 path
- Confirm `nostr:nip013` entries appear in same file.
4. Trigger normal didactyl LLM call
- Confirm `[didactyl] llm request` still lands in same file.
5. Verify there is no second active file for nostr core logs.
## Rollout Notes
- Implement bridge first, then verify in development run.
- If log volume is too high, reduce callback threshold by setting `nostr_set_log_level`.
- Keep one destination file operationally stable by configuring and documenting `DIDACTYL_LOG_FILE`.
## Files to Change in Code Mode
- [`src/nostr_handler.c`](../src/nostr_handler.c)
- Optional docs update in [`README.md`](../README.md)
- Optional operational note in [`docs/TOOLS.md`](../docs/TOOLS.md)

View File

@@ -1,14 +1,31 @@
#include "debug.h"
#include <stdarg.h>
#include <stdlib.h>
#include <string.h>
debug_level_t g_debug_level = DEBUG_LEVEL_NONE;
static FILE* g_debug_file = NULL;
static void debug_open_log_file(void) {
if (g_debug_file) return;
const char* path = getenv("DIDACTYL_LOG_FILE");
if (!path || path[0] == '\0') {
path = "debug.log";
}
g_debug_file = fopen(path, "a");
if (g_debug_file) {
setvbuf(g_debug_file, NULL, _IOLBF, 0);
}
}
void debug_init(int level) {
if (level < 0) level = 0;
if (level > 5) level = 5;
g_debug_level = (debug_level_t)level;
debug_open_log_file();
}
void debug_log(debug_level_t level, const char* file, int line, const char* format, ...) {
@@ -28,11 +45,17 @@ void debug_log(debug_level_t level, const char* file, int line, const char* form
}
printf("[%s] [%s] ", timestamp, level_str);
if (g_debug_file) {
fprintf(g_debug_file, "[%s] [%s] ", timestamp, level_str);
}
if (file && g_debug_level >= DEBUG_LEVEL_TRACE) {
const char* filename = strrchr(file, '/');
filename = filename ? filename + 1 : file;
printf("[%s:%d] ", filename, line);
if (g_debug_file) {
fprintf(g_debug_file, "[%s:%d] ", filename, line);
}
}
va_list args;
@@ -40,6 +63,17 @@ void debug_log(debug_level_t level, const char* file, int line, const char* form
vprintf(format, args);
va_end(args);
if (g_debug_file) {
va_list args_file;
va_start(args_file, format);
vfprintf(g_debug_file, format, args_file);
va_end(args_file);
}
printf("\n");
fflush(stdout);
if (g_debug_file) {
fprintf(g_debug_file, "\n");
fflush(g_debug_file);
}
}

View File

@@ -10,6 +10,7 @@
#include <unistd.h>
#include "cjson/cJSON.h"
#include "debug.h"
typedef struct {
char* data;
@@ -92,11 +93,9 @@ static char* perform_http_request(const char* url, const char* body, int is_post
}
if (url_looks_like_websocket(url)) {
fprintf(stderr,
"[didactyl] llm config error: base_url must be HTTP(S), got WebSocket URL: %s\n",
url);
fprintf(stderr,
"[didactyl] llm hint: set llm.base_url to an OpenAI-compatible HTTPS endpoint, e.g. https://api.example.com/v1\n");
DEBUG_ERROR("[didactyl] llm config error: base_url must be HTTP(S), got WebSocket URL: %s",
url);
DEBUG_WARN("[didactyl] llm hint: set llm.base_url to an OpenAI-compatible HTTPS endpoint, e.g. https://api.example.com/v1");
curl_easy_cleanup(curl);
return NULL;
}
@@ -129,14 +128,13 @@ static char* perform_http_request(const char* url, const char* body, int is_post
if (is_post) {
size_t body_len = body ? strlen(body) : 0U;
size_t body_preview_len = body_len > 4000U ? 4000U : body_len;
fprintf(stderr,
"[didactyl] llm request: method=POST url=%s body_bytes=%zu body_preview=%.4000s%s\n",
url,
body_len,
body ? body : "",
body_len > body_preview_len ? "..." : "");
DEBUG_INFO("[didactyl] llm request: method=POST url=%s body_bytes=%zu body_preview=%.4000s%s",
url,
body_len,
body ? body : "",
body_len > body_preview_len ? "..." : "");
} else {
fprintf(stderr, "[didactyl] llm request: method=GET url=%s\n", url);
DEBUG_INFO("[didactyl] llm request: method=GET url=%s", url);
}
CURLcode res = curl_easy_perform(curl);
@@ -147,33 +145,32 @@ static char* perform_http_request(const char* url, const char* body, int is_post
curl_easy_cleanup(curl);
if (res != CURLE_OK) {
fprintf(stderr, "[didactyl] llm http request failed: curl=%s\n", curl_easy_strerror(res));
DEBUG_ERROR("[didactyl] llm http request failed: curl=%s", curl_easy_strerror(res));
if (rb.data && rb.len > 0) {
fprintf(stderr, "[didactyl] llm partial response: %.600s%s\n",
rb.data,
rb.len > 600 ? "..." : "");
DEBUG_WARN("[didactyl] llm partial response: %.600s%s",
rb.data,
rb.len > 600 ? "..." : "");
}
free(rb.data);
return NULL;
}
if (status < 200 || status >= 300) {
fprintf(stderr, "[didactyl] llm http request failed: status=%ld\n", status);
DEBUG_ERROR("[didactyl] llm http request failed: status=%ld", status);
if (status == 101) {
fprintf(stderr,
"[didactyl] llm hint: received HTTP 101 (Switching Protocols), which usually means llm.base_url points to a WebSocket server instead of an HTTP LLM API\n");
DEBUG_WARN("[didactyl] llm hint: received HTTP 101 (Switching Protocols), which usually means llm.base_url points to a WebSocket server instead of an HTTP LLM API");
}
if (rb.data && rb.len > 0) {
fprintf(stderr, "[didactyl] llm error response: %.1200s%s\n",
rb.data,
rb.len > 1200 ? "..." : "");
DEBUG_WARN("[didactyl] llm error response: %.1200s%s",
rb.data,
rb.len > 1200 ? "..." : "");
}
free(rb.data);
return NULL;
}
if (!rb.data) {
fprintf(stderr, "[didactyl] llm http request failed: empty response body\n");
DEBUG_ERROR("[didactyl] llm http request failed: empty response body");
return NULL;
}
@@ -332,9 +329,9 @@ char* llm_chat(const char* system_prompt, const char* user_message) {
llm_response_t parsed;
if (parse_llm_response(raw, &parsed) != 0) {
fprintf(stderr, "[didactyl] failed to parse llm response (non-tool path): %.1200s%s\n",
raw,
strlen(raw) > 1200 ? "..." : "");
DEBUG_ERROR("[didactyl] failed to parse llm response (non-tool path): %.1200s%s",
raw,
strlen(raw) > 1200 ? "..." : "");
free(raw);
return NULL;
}
@@ -393,9 +390,8 @@ int llm_chat_with_tools_messages(const char* messages_json,
}
if (filtered_count > 0) {
fprintf(stderr,
"[didactyl] llm request sanitizer: removed %d empty text message(s) before provider request\n",
filtered_count);
DEBUG_INFO("[didactyl] llm request sanitizer: removed %d empty text message(s) before provider request",
filtered_count);
}
cJSON_AddItemToObject(root, "messages", messages);
@@ -420,9 +416,9 @@ int llm_chat_with_tools_messages(const char* messages_json,
int rc = parse_llm_response(raw, out_response);
if (rc != 0) {
fprintf(stderr, "[didactyl] failed to parse llm response (tools path): %.1200s%s\n",
raw,
strlen(raw) > 1200 ? "..." : "");
DEBUG_ERROR("[didactyl] failed to parse llm response (tools path): %.1200s%s",
raw,
strlen(raw) > 1200 ? "..." : "");
}
free(raw);
return rc;

View File

@@ -12,8 +12,8 @@
// Using DIDACTYL_ prefix to avoid conflicts with nostr_core_lib VERSION macros
#define DIDACTYL_VERSION_MAJOR 0
#define DIDACTYL_VERSION_MINOR 0
#define DIDACTYL_VERSION_PATCH 70
#define DIDACTYL_VERSION "v0.0.70"
#define DIDACTYL_VERSION_PATCH 71
#define DIDACTYL_VERSION "v0.0.71"
// Agent metadata
#define DIDACTYL_NAME "Didactyl"

View File

@@ -70,6 +70,58 @@ static pthread_mutex_t g_dm_dedup_mutex = PTHREAD_MUTEX_INITIALIZER;
#define SENDER_PROTOCOL_CACHE_SIZE 128
#define DM_HISTORY_RING_SIZE 256
static int didactyl_debug_to_nostr_log_level(void) {
switch (g_debug_level) {
case DEBUG_LEVEL_TRACE: return NOSTR_LOG_LEVEL_TRACE;
case DEBUG_LEVEL_DEBUG: return NOSTR_LOG_LEVEL_DEBUG;
case DEBUG_LEVEL_INFO: return NOSTR_LOG_LEVEL_INFO;
case DEBUG_LEVEL_WARN: return NOSTR_LOG_LEVEL_WARN;
case DEBUG_LEVEL_ERROR: return NOSTR_LOG_LEVEL_ERROR;
case DEBUG_LEVEL_NONE:
default:
return NOSTR_LOG_LEVEL_ERROR;
}
}
static debug_level_t nostr_log_to_didactyl_level(int level) {
switch (level) {
case NOSTR_LOG_LEVEL_TRACE: return DEBUG_LEVEL_TRACE;
case NOSTR_LOG_LEVEL_DEBUG: return DEBUG_LEVEL_DEBUG;
case NOSTR_LOG_LEVEL_INFO: return DEBUG_LEVEL_INFO;
case NOSTR_LOG_LEVEL_WARN: return DEBUG_LEVEL_WARN;
case NOSTR_LOG_LEVEL_ERROR:
default:
return DEBUG_LEVEL_ERROR;
}
}
static void nostr_core_log_bridge(int level, const char* component, const char* message, void* user_data) {
(void)user_data;
debug_level_t mapped = nostr_log_to_didactyl_level(level);
const char* comp = (component && component[0] != '\0') ? component : "core";
const char* msg = message ? message : "";
switch (mapped) {
case DEBUG_LEVEL_TRACE:
DEBUG_TRACE("[didactyl] [nostr:%s] %s", comp, msg);
break;
case DEBUG_LEVEL_DEBUG:
DEBUG_LOG("[didactyl] [nostr:%s] %s", comp, msg);
break;
case DEBUG_LEVEL_INFO:
DEBUG_INFO("[didactyl] [nostr:%s] %s", comp, msg);
break;
case DEBUG_LEVEL_WARN:
DEBUG_WARN("[didactyl] [nostr:%s] %s", comp, msg);
break;
case DEBUG_LEVEL_ERROR:
default:
DEBUG_ERROR("[didactyl] [nostr:%s] %s", comp, msg);
break;
}
}
typedef struct {
char pubkey_hex[65];
dm_protocol_t protocol;
@@ -1534,6 +1586,9 @@ int nostr_handler_init(didactyl_config_t* config) {
DEBUG_INFO("[didactyl] initializing relay pool with %d relays", g_cfg->relay_count);
nostr_set_log_callback(nostr_core_log_bridge, NULL);
nostr_set_log_level((nostr_log_level_t)didactyl_debug_to_nostr_log_level());
nostr_pool_reconnect_config_t reconnect = *nostr_pool_reconnect_config_default();
reconnect.enable_auto_reconnect = 1;
reconnect.ping_interval_seconds = 20;
@@ -3050,6 +3105,8 @@ void nostr_handler_cleanup(void) {
nostr_relay_pool_destroy(g_pool);
}
nostr_set_log_callback(NULL, NULL);
free(g_last_relay_statuses);
g_last_relay_statuses = NULL;
free(g_startup_published);

View File

@@ -3,6 +3,7 @@
#include "tools_internal.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "cjson/cJSON.h"
@@ -47,6 +48,9 @@ char* execute_nostr_encode(const char* args_json) {
cJSON* type = cJSON_GetObjectItemCaseSensitive(args, "type");
cJSON* hex = cJSON_GetObjectItemCaseSensitive(args, "hex");
cJSON* relays = cJSON_GetObjectItemCaseSensitive(args, "relays");
cJSON* kind = cJSON_GetObjectItemCaseSensitive(args, "kind");
cJSON* identifier = cJSON_GetObjectItemCaseSensitive(args, "identifier");
if (!type || !cJSON_IsString(type) || !type->valuestring ||
!hex || !cJSON_IsString(hex) || !hex->valuestring || !is_hex_string_len_local(hex->valuestring, 64U)) {
@@ -63,28 +67,89 @@ char* execute_nostr_encode(const char* args_json) {
return json_error_local("nostr_encode invalid hex");
}
const char** relay_ptrs = NULL;
int relay_count = 0;
if (relays) {
if (!cJSON_IsArray(relays)) {
cJSON_Delete(args);
return json_error_local("nostr_encode relays must be an array of relay URLs");
}
relay_count = cJSON_GetArraySize(relays);
if (relay_count > 0) {
relay_ptrs = (const char**)malloc((size_t)relay_count * sizeof(char*));
if (!relay_ptrs) {
cJSON_Delete(args);
return json_error_local("memory allocation failed");
}
for (int i = 0; i < relay_count; i++) {
cJSON* relay = cJSON_GetArrayItem(relays, i);
if (!relay || !cJSON_IsString(relay) || !relay->valuestring || relay->valuestring[0] == '\0') {
free(relay_ptrs);
cJSON_Delete(args);
return json_error_local("nostr_encode relays must contain non-empty strings");
}
relay_ptrs[i] = relay->valuestring;
}
}
}
char bech32[256] = {0};
char uri[1200] = {0};
int rc = NOSTR_ERROR_INVALID_INPUT;
if (strcmp(type_name, "npub") == 0) {
rc = nostr_key_to_bech32(key_bytes, "npub", bech32);
if (rc == NOSTR_SUCCESS) {
snprintf(uri, sizeof(uri), "nostr:%s", bech32);
}
} else if (strcmp(type_name, "nsec") == 0) {
rc = nostr_key_to_bech32(key_bytes, "nsec", bech32);
if (rc == NOSTR_SUCCESS) {
snprintf(uri, sizeof(uri), "nostr:%s", bech32);
}
} else if (strcmp(type_name, "note") == 0) {
rc = nostr_key_to_bech32(key_bytes, "note", bech32);
if (rc == NOSTR_SUCCESS) {
snprintf(uri, sizeof(uri), "nostr:%s", bech32);
}
} else if (strcmp(type_name, "nprofile") == 0) {
rc = nostr_build_uri_nprofile(key_bytes, relay_ptrs, relay_count, uri, sizeof(uri));
} else if (strcmp(type_name, "nevent") == 0) {
int kind_value = -1;
if (kind) {
if (!cJSON_IsNumber(kind)) {
free(relay_ptrs);
cJSON_Delete(args);
return json_error_local("nostr_encode nevent kind must be an integer");
}
kind_value = (int)kind->valuedouble;
}
rc = nostr_build_uri_nevent(key_bytes, relay_ptrs, relay_count, NULL, kind_value, 0, uri, sizeof(uri));
} else if (strcmp(type_name, "naddr") == 0) {
if (!kind || !cJSON_IsNumber(kind)) {
free(relay_ptrs);
cJSON_Delete(args);
return json_error_local("nostr_encode naddr requires kind integer");
}
if (!identifier || !cJSON_IsString(identifier) || !identifier->valuestring || identifier->valuestring[0] == '\0') {
free(relay_ptrs);
cJSON_Delete(args);
return json_error_local("nostr_encode naddr requires non-empty identifier string");
}
rc = nostr_build_uri_naddr(identifier->valuestring, key_bytes, (int)kind->valuedouble,
relay_ptrs, relay_count, uri, sizeof(uri));
} else {
free(relay_ptrs);
cJSON_Delete(args);
return json_error_local("nostr_encode currently supports npub, nsec, note");
return json_error_local("nostr_encode currently supports npub, nsec, note, nprofile, nevent, naddr");
}
free(relay_ptrs);
cJSON_Delete(args);
if (rc != NOSTR_SUCCESS) {
return json_error_local("nostr_encode failed");
}
char uri[320] = {0};
snprintf(uri, sizeof(uri), "nostr:%s", bech32);
cJSON* out = cJSON_CreateObject();
if (!out) return NULL;
cJSON_AddBoolToObject(out, "success", 1);

View File

@@ -19,10 +19,35 @@ if [ -z "$LOG_FILE" ] || [ ! -f "$LOG_FILE" ]; then
exit 1
fi
echo "Following API call logs from: $LOG_FILE"
echo "Following numbered LLM API calls from: $LOG_FILE"
echo "(Press Ctrl+C to stop)"
# Show request/response failure lines related to outbound LLM HTTP calls.
# These are emitted by Didactyl in src/llm.c via fprintf(stderr, ...).
# Count only outbound provider calls from src/llm.c:
# [didactyl] llm request: method=... url=...
START_API=$(tail -n 20000 "$LOG_FILE" | grep -Ec "\\[didactyl\\] llm request:" || true)
[ -z "$START_API" ] && START_API=0
echo "Starting api_call count from: $START_API"
tail -n 0 -F "$LOG_FILE" \
| stdbuf -oL grep -E --line-buffered "\\[didactyl\\] llm request:|\\[didactyl\\] llm http request failed:|\\[didactyl\\] llm error response:|\\[didactyl\\] llm partial response:|\\[didactyl\\] llm hint:"
| awk -v api_start="$START_API" '
BEGIN { api = api_start }
{
line = $0
if (line !~ /\[didactyl\] llm request:/) next
api++
method = "?"
url = "?"
if (match(line, /method=[A-Z]+/)) {
method = substr(line, RSTART + 7, RLENGTH - 7)
}
if (match(line, /url=[^ ]+/)) {
url = substr(line, RSTART + 4, RLENGTH - 4)
}
printf "%d) %s %s\n", api, method, url
fflush()
}
'