v0.2.30 - Implemented NIP-17 bootstrap/discovery (kind 10050) and added seal/rumor pubkey verification

This commit is contained in:
Didactyl User
2026-04-09 07:19:40 -04:00
parent d7c009ad8f
commit d59493992b
14 changed files with 1085 additions and 15037 deletions
+2 -2
View File
@@ -54,11 +54,11 @@ Skills compose by adoption-list order (`10123`) and trigger tags carry runtime e
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.2.29
## Current Status — v0.2.30
**Active build — this project is barely working. Experiment at your own risk.**
> Last release update: v0.2.29Fix self-skill EOSE startup deadlock by adding per-relay timeout fallback in nostr core relay pool
> Last release update: v0.2.30Implemented NIP-17 bootstrap/discovery (kind 10050) and added seal/rumor pubkey verification
- Connects to configured relays with auto-reconnect and relay state transition logging
- Publishes configured startup events per relay as each relay becomes connected
+35 -15004
View File
File diff suppressed because one or more lines are too long
+8
View File
@@ -60,6 +60,14 @@
["r", "wss://relay.primal.net"]
]
},
{
"kind": 10050,
"content": "",
"tags": [
["relay", "wss://relay.damus.io"],
["relay", "wss://relay.primal.net"]
]
},
{
"kind": 31124,
"content": "# Didactyl Agent\n\nYou are {{my_kind0_profile}}\n\nYour npub: {{my_npub}}\n\n## Rules\n\n- Communicate through encrypted Nostr direct messages\n- Keep responses concise and clear\n- Be helpful and technically accurate\n- If unsure, state uncertainty directly\n- Use tools when a request requires taking action\n- After a tool call, base your answer on the actual tool result\n- Never claim a tool was run if no tool was executed\n- Maintain your task list as short-term working memory\n- Never reveal your private key (nsec)\n- You may share your public key (npub) with anyone",
+9
View File
@@ -45,6 +45,15 @@
["r", "wss://relay.laantungir.net"]
]
},
{
"kind": 10050,
"content": "",
"tags": [
["relay", "wss://relay.damus.io"],
["relay", "wss://relay.primal.net"],
["relay", "wss://relay.laantungir.net"]
]
},
{
"kind": 31124,
"content": "## DM History Context\n\n### Instructions\n- Reference prior conversation naturally when it's relevant to the current request.\n- Do not repeat entire DM history back to the user unless explicitly asked.\n- Use this context to avoid asking questions that were already answered in recent messages.\n\n### Recent DM History (last 10 messages)\n\n{{nostr_dm_history({\"limit\":10,\"format\":\"text\"})}}",
+24 -17
View File
@@ -2,7 +2,7 @@
## Background
Didactyl currently uses NIP-04 (kind 4) for all DM communication. NIP-17 send support exists via `nostr_handler_send_dm_nip17()` and the `nostr_dm_send_nip17` tool, but there is **no ability to receive NIP-17 messages** and **all agent responses always use NIP-04**.
Didactyl now supports both NIP-04 and NIP-17 DM flows (including receive path for kind 1059 and protocol-aware auto-reply routing). Remaining work in this plan tracks spec-hardening and bootstrap/discovery details.
The `nostr_core_lib` already has full NIP-17/NIP-59 support including:
- `nostr_nip17_create_chat_event()` — create kind 14 rumor
@@ -113,28 +113,35 @@ Replace all `nostr_handler_send_dm()` calls with `nostr_handler_send_dm_auto()`.
### 7. Add kind 10050 startup event support
**File**: `config.json.example`
**Status**: ✅ Implemented
Add a kind 10050 startup event example so NIP-17 clients can discover the agent's DM relay preferences:
**Files**:
- `src/default_events.h` (default startup kinds now include `10050`)
- `src/setup_wizard.c` (builds kind `10050` tags from current configured relays)
- `genesis.jsonc.example` (includes kind `10050` startup event)
- `genesis.lt.didactyl.jsonc` (includes kind `10050` startup event)
```json
{
"kind": 10050,
"content": "",
"tags": [
["relay", "wss://relay.damus.io"],
["relay", "wss://nos.lol"]
]
}
```
**File**: `config.c` — startup event parsing already handles arbitrary kinds, so this should work with no code changes.
Implementation detail: kind `10050` reuses the same relay set as kind `10002`/current config relays, with `relay` tags as required by NIP-17.
### 8. Update startup DM to use auto-routing
**File**: `main.c`
**Status**: ✅ Implemented
The startup status DM at line 255 currently calls `nostr_handler_send_dm()`. Update to `nostr_handler_send_dm_auto()`.
`main.c` uses `nostr_handler_send_dm_auto()` for startup DM flow.
### 9. Add NIP-17 seal/rumor pubkey verification
**Status**: ✅ Implemented (in core lib)
**File**: `nostr_core_lib/nostr_core/nip017.c`
`nostr_nip17_receive_dm()` now verifies that seal pubkey matches rumor pubkey and rejects mismatches to prevent sender impersonation.
### 10. Recipient kind-10050 relay targeting for sends
**Status**: ⏸ Deferred
Current NIP-17 send path publishes to connected configured relays. Querying and targeting recipient-advertised kind `10050` relays is kept as a future enhancement while debugging current interoperability.
## Architecture Diagram
+112 -4
View File
@@ -252,6 +252,38 @@ static int append_tool_result_message(cJSON* messages, const char* tool_call_id,
return 0;
}
static int maybe_append_loaded_skill_instructions(cJSON* messages,
const char* tool_name,
const char* tool_result_json) {
if (!messages || !tool_name || strcmp(tool_name, "skill_load") != 0 || !tool_result_json) {
return 0;
}
cJSON* root = cJSON_Parse(tool_result_json);
if (!root || !cJSON_IsObject(root)) {
cJSON_Delete(root);
return 0;
}
cJSON* ok = cJSON_GetObjectItemCaseSensitive(root, "success");
cJSON* type = cJSON_GetObjectItemCaseSensitive(root, "type");
cJSON* instructions = cJSON_GetObjectItemCaseSensitive(root, "instructions");
int should_apply = (ok && cJSON_IsBool(ok) && cJSON_IsTrue(ok) &&
type && cJSON_IsString(type) && type->valuestring &&
strcmp(type->valuestring, "skill_instructions") == 0 &&
instructions && cJSON_IsString(instructions) && instructions->valuestring &&
instructions->valuestring[0] != '\0');
int rc = 0;
if (should_apply) {
rc = append_simple_message(messages, "system", instructions->valuestring);
}
cJSON_Delete(root);
return rc;
}
static char* local_json_error(const char* msg) {
cJSON* out = cJSON_CreateObject();
if (!out) return NULL;
@@ -292,6 +324,51 @@ static char* build_slash_args_json(const char* raw_args) {
return out;
}
static char* build_run_args_json_local(const char* raw_args) {
const char* s = skip_spaces_local(raw_args);
if (!s || s[0] == '\0') {
return local_json_error("/run requires a skill identifier");
}
const char* ident_start = s;
while (*s && !is_space_char(*s)) s++;
size_t ident_len = (size_t)(s - ident_start);
if (ident_len == 0 || ident_len >= 256U) {
return local_json_error("/run skill identifier is invalid");
}
char ident[256];
memcpy(ident, ident_start, ident_len);
ident[ident_len] = '\0';
const char* remaining = skip_spaces_local(s);
cJSON* out = cJSON_CreateObject();
if (!out) return NULL;
const char* c1 = strchr(ident, ':');
const char* c2 = c1 ? strchr(c1 + 1, ':') : NULL;
if (c1 && c2 && c1 != ident && (size_t)(c2 - c1) == 65U && c2[1] != '\0') {
size_t pubkey_len = (size_t)(c2 - (c1 + 1));
char pubkey[65];
memcpy(pubkey, c1 + 1, pubkey_len);
pubkey[pubkey_len] = '\0';
cJSON_AddStringToObject(out, "pubkey", pubkey);
cJSON_AddStringToObject(out, "d_tag", c2 + 1);
} else {
cJSON_AddStringToObject(out, "d_tag", ident);
}
if (remaining && remaining[0] != '\0') {
cJSON_AddStringToObject(out, "args", remaining);
}
char* json = cJSON_PrintUnformatted(out);
cJSON_Delete(out);
return json;
}
static int append_textf_local(char** buf, size_t* cap, size_t* used, const char* fmt, ...) {
if (!buf || !cap || !used || !fmt) return -1;
@@ -794,7 +871,8 @@ static int handle_slash_command(const char* sender_pubkey_hex, const char* messa
char* result_json = NULL;
if (raw_args && (strcmp(raw_args, "-h") == 0 || strcmp(raw_args, "--help") == 0)) {
result_json = build_slash_help_tool_json(tool_name);
const char* help_name = (strcmp(tool_name, "run") == 0) ? "skill_run" : tool_name;
result_json = build_slash_help_tool_json(help_name);
} else if (strcmp(tool_name, "help") == 0) {
if (!raw_args || raw_args[0] == '\0') {
result_json = build_slash_help_all_json();
@@ -806,14 +884,24 @@ static int handle_slash_command(const char* sender_pubkey_hex, const char* messa
i++;
}
help_tool[i] = '\0';
result_json = build_slash_help_tool_json(help_tool);
const char* help_name = (strcmp(help_tool, "run") == 0) ? "skill_run" : help_tool;
result_json = build_slash_help_tool_json(help_name);
}
} else {
char* args_json = build_slash_args_json(raw_args);
char* args_json = NULL;
const char* exec_tool_name = tool_name;
if (strcmp(tool_name, "run") == 0) {
args_json = build_run_args_json_local(raw_args);
exec_tool_name = "skill_run";
} else {
args_json = build_slash_args_json(raw_args);
}
if (!args_json) {
result_json = local_json_error("failed to parse slash args");
} else {
result_json = tools_execute(&g_tools_ctx, tool_name, args_json);
result_json = tools_execute(&g_tools_ctx, exec_tool_name, args_json);
free(args_json);
}
}
@@ -1864,6 +1952,17 @@ void agent_on_trigger(const char* skill_d_tag,
return;
}
if (maybe_append_loaded_skill_instructions(messages, tc->name, tool_result) != 0) {
free(tool_result);
llm_response_free(&resp);
cJSON_Delete(messages);
free(tools_json);
g_tools_ctx.template_trigger_event_json = prev_trigger_event_json;
free(event_json);
(void)nostr_handler_send_dm_auto(g_cfg->admin.pubkey, "Triggered skill execution failed: loaded skill instruction append failed.");
return;
}
free(tool_result);
}
@@ -2161,6 +2260,15 @@ void agent_on_message(const char* sender_pubkey_hex,
(void)nostr_handler_send_dm_auto(sender_pubkey_hex, "Failed to append tool result.");
return;
}
if (maybe_append_loaded_skill_instructions(messages, tc->name, tool_result) != 0) {
free(tool_result);
llm_response_free(&resp);
cJSON_Delete(messages);
free(tools_json);
free(dm_context);
(void)nostr_handler_send_dm_auto(sender_pubkey_hex, "Failed to apply loaded skill instructions.");
return;
}
free(tool_result);
}
+4 -1
View File
@@ -12,11 +12,13 @@
#define DIDACTYL_STARTUP_KIND_PROFILE 0
#define DIDACTYL_STARTUP_KIND_CONTACTS 3
#define DIDACTYL_STARTUP_KIND_RELAYS 10002
#define DIDACTYL_STARTUP_KIND_DM_RELAYS 10050
static const int DIDACTYL_DEFAULT_STARTUP_EVENT_KINDS[] = {
DIDACTYL_STARTUP_KIND_PROFILE,
DIDACTYL_STARTUP_KIND_CONTACTS,
DIDACTYL_STARTUP_KIND_RELAYS
DIDACTYL_STARTUP_KIND_RELAYS,
DIDACTYL_STARTUP_KIND_DM_RELAYS
};
#define DIDACTYL_DEFAULT_STARTUP_EVENT_KIND_COUNT ((int)(sizeof(DIDACTYL_DEFAULT_STARTUP_EVENT_KINDS) / sizeof(DIDACTYL_DEFAULT_STARTUP_EVENT_KINDS[0])))
@@ -25,6 +27,7 @@ static const int DIDACTYL_DEFAULT_STARTUP_EVENT_KINDS[] = {
#define DIDACTYL_DEFAULT_KIND3_CONTENT ""
#define DIDACTYL_DEFAULT_KIND3_TAGS_JSON_TEMPLATE "[[\"p\",\"%s\"]]"
#define DIDACTYL_DEFAULT_KIND10002_CONTENT ""
#define DIDACTYL_DEFAULT_KIND10050_CONTENT ""
static const char* DIDACTYL_DEFAULT_RELAYS[] = {
"wss://relay.damus.io",
+101 -4
View File
@@ -400,6 +400,38 @@ static int append_tool_result_message_local(cJSON* messages, const char* tool_ca
return 0;
}
static int maybe_append_loaded_skill_instructions_local(cJSON* messages,
const char* tool_name,
const char* tool_result_json) {
if (!messages || !tool_name || strcmp(tool_name, "skill_load") != 0 || !tool_result_json) {
return 0;
}
cJSON* root = cJSON_Parse(tool_result_json);
if (!root || !cJSON_IsObject(root)) {
cJSON_Delete(root);
return 0;
}
cJSON* ok = cJSON_GetObjectItemCaseSensitive(root, "success");
cJSON* type = cJSON_GetObjectItemCaseSensitive(root, "type");
cJSON* instructions = cJSON_GetObjectItemCaseSensitive(root, "instructions");
int should_apply = (ok && cJSON_IsBool(ok) && cJSON_IsTrue(ok) &&
type && cJSON_IsString(type) && type->valuestring &&
strcmp(type->valuestring, "skill_instructions") == 0 &&
instructions && cJSON_IsString(instructions) && instructions->valuestring &&
instructions->valuestring[0] != '\0');
int rc = 0;
if (should_apply) {
rc = append_simple_message_local(messages, "system", instructions->valuestring);
}
cJSON_Delete(root);
return rc;
}
static int is_space_char_local(char c) {
return c == ' ' || c == '\t' || c == '\n' || c == '\r';
}
@@ -440,6 +472,51 @@ static char* build_slash_args_json_local(const char* raw_args) {
return out;
}
static char* build_run_args_json_local(const char* raw_args) {
const char* s = skip_spaces_local(raw_args);
if (!s || s[0] == '\0') {
return json_error_string_local("/run requires a skill identifier");
}
const char* ident_start = s;
while (*s && !is_space_char_local(*s)) s++;
size_t ident_len = (size_t)(s - ident_start);
if (ident_len == 0 || ident_len >= 256U) {
return json_error_string_local("/run skill identifier is invalid");
}
char ident[256];
memcpy(ident, ident_start, ident_len);
ident[ident_len] = '\0';
const char* remaining = skip_spaces_local(s);
cJSON* out = cJSON_CreateObject();
if (!out) return NULL;
const char* c1 = strchr(ident, ':');
const char* c2 = c1 ? strchr(c1 + 1, ':') : NULL;
if (c1 && c2 && c1 != ident && (size_t)(c2 - c1) == 65U && c2[1] != '\0') {
size_t pubkey_len = (size_t)(c2 - (c1 + 1));
char pubkey[65];
memcpy(pubkey, c1 + 1, pubkey_len);
pubkey[pubkey_len] = '\0';
cJSON_AddStringToObject(out, "pubkey", pubkey);
cJSON_AddStringToObject(out, "d_tag", c2 + 1);
} else {
cJSON_AddStringToObject(out, "d_tag", ident);
}
if (remaining && remaining[0] != '\0') {
cJSON_AddStringToObject(out, "args", remaining);
}
char* json = cJSON_PrintUnformatted(out);
cJSON_Delete(out);
return json;
}
static int append_textf_http(char** buf, size_t* cap, size_t* used, const char* fmt, ...) {
if (!buf || !cap || !used || !fmt) return -1;
@@ -853,7 +930,8 @@ static char* execute_slash_command_http(const char* message) {
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);
const char* help_name = (strcmp(tool_name, "run") == 0) ? "skill_run" : tool_name;
result_json = build_slash_help_tool_json_local(help_name);
} else if (strcmp(tool_name, "help") == 0) {
if (!raw_args || raw_args[0] == '\0') {
result_json = build_slash_help_all_json_local();
@@ -865,14 +943,24 @@ static char* execute_slash_command_http(const char* message) {
i++;
}
help_tool[i] = '\0';
result_json = build_slash_help_tool_json_local(help_tool);
const char* help_name = (strcmp(help_tool, "run") == 0) ? "skill_run" : help_tool;
result_json = build_slash_help_tool_json_local(help_name);
}
} else {
char* args_json = build_slash_args_json_local(raw_args);
char* args_json = NULL;
const char* exec_tool_name = tool_name;
if (strcmp(tool_name, "run") == 0) {
args_json = build_run_args_json_local(raw_args);
exec_tool_name = "skill_run";
} else {
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);
result_json = tools_execute(g_api_ctx.tools_ctx, exec_tool_name, args_json);
free(args_json);
}
}
@@ -1004,6 +1092,15 @@ static cJSON* run_prompt_with_tools_convo(cJSON* convo,
final_response = strdup("Failed to append tool result.");
goto done;
}
if (maybe_append_loaded_skill_instructions_local(convo,
tc->name,
tool_result) != 0) {
free(tool_result);
llm_response_free(&resp);
final_response = strdup("Failed to apply loaded skill instructions.");
goto done;
}
free(tool_result);
}
+2 -2
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 2
#define DIDACTYL_VERSION_PATCH 29
#define DIDACTYL_VERSION "v0.2.29"
#define DIDACTYL_VERSION_PATCH 30
#define DIDACTYL_VERSION "v0.2.30"
// Agent metadata
#define DIDACTYL_NAME "Didactyl"
+30 -3
View File
@@ -346,8 +346,12 @@ static int set_default_relays(didactyl_config_t* cfg) {
return 0;
}
static int build_kind10002_tags_json(const didactyl_config_t* cfg, char** out_tags_json) {
if (!cfg || !out_tags_json || cfg->relay_count <= 0 || !cfg->relays) return -1;
static int build_relay_tags_json_with_key(const didactyl_config_t* cfg,
const char* tag_key,
char** out_tags_json) {
if (!cfg || !tag_key || tag_key[0] == '\0' || !out_tags_json || cfg->relay_count <= 0 || !cfg->relays) {
return -1;
}
*out_tags_json = NULL;
cJSON* tags = cJSON_CreateArray();
@@ -360,7 +364,7 @@ static int build_kind10002_tags_json(const didactyl_config_t* cfg, char** out_ta
cJSON_Delete(tags);
return -1;
}
cJSON_AddItemToArray(tag, cJSON_CreateString("r"));
cJSON_AddItemToArray(tag, cJSON_CreateString(tag_key));
cJSON_AddItemToArray(tag, cJSON_CreateString(cfg->relays[i]));
cJSON_AddItemToArray(tags, tag);
}
@@ -372,6 +376,14 @@ static int build_kind10002_tags_json(const didactyl_config_t* cfg, char** out_ta
return 0;
}
static int build_kind10002_tags_json(const didactyl_config_t* cfg, char** out_tags_json) {
return build_relay_tags_json_with_key(cfg, "r", out_tags_json);
}
static int build_kind10050_tags_json(const didactyl_config_t* cfg, char** out_tags_json) {
return build_relay_tags_json_with_key(cfg, "relay", out_tags_json);
}
static int tags_json_get_d_tag_local(const char* tags_json, char* out, size_t out_size) {
if (!tags_json || !out || out_size == 0) {
return -1;
@@ -537,6 +549,21 @@ static int apply_default_startup_events(didactyl_config_t* cfg, const char* agen
}
continue;
}
if (kind == DIDACTYL_STARTUP_KIND_DM_RELAYS) {
char* tags_json = NULL;
if (build_kind10050_tags_json(cfg, &tags_json) != 0 || !tags_json) {
free(tags_json);
return -1;
}
int rc = upsert_startup_event(cfg, kind, DIDACTYL_DEFAULT_KIND10050_CONTENT, tags_json);
free(tags_json);
if (rc != 0) {
return -1;
}
continue;
}
}
return 0;
+667
View File
@@ -12,6 +12,7 @@
#include "../nostr_handler.h"
#include "../trigger_manager.h"
#include "../agent.h"
#include "../llm.h"
static char* json_error_local(const char* msg) {
cJSON* root = cJSON_CreateObject();
@@ -254,6 +255,40 @@ static int validate_skill_d_tag_local(const char* d_tag) {
return 1;
}
static void apply_llm_spec_local_to_config(const char* llm_spec, llm_config_t* cfg) {
if (!llm_spec || !cfg) return;
const char* spec = llm_spec;
while (*spec && isspace((unsigned char)*spec)) spec++;
const char* comma = strchr(spec, ',');
size_t len = comma ? (size_t)(comma - spec) : strlen(spec);
while (len > 0 && isspace((unsigned char)spec[len - 1])) len--;
const char* slash = memchr(spec, '/', len);
if (slash) {
size_t provider_len = (size_t)(slash - spec);
const char* model = slash + 1;
size_t model_len = len - (size_t)(model - spec);
while (provider_len > 0 && isspace((unsigned char)spec[provider_len - 1])) provider_len--;
while (model_len > 0 && isspace((unsigned char)*model)) {
model++;
model_len--;
}
while (model_len > 0 && isspace((unsigned char)model[model_len - 1])) model_len--;
if (provider_len > 0 && provider_len < sizeof(cfg->provider)) {
snprintf(cfg->provider, sizeof(cfg->provider), "%.*s", (int)provider_len, spec);
}
if (model_len > 0 && model_len < sizeof(cfg->model)) {
snprintf(cfg->model, sizeof(cfg->model), "%.*s", (int)model_len, model);
}
} else if (len > 0 && len < sizeof(cfg->model)) {
snprintf(cfg->model, sizeof(cfg->model), "%.*s", (int)len, spec);
}
}
static int ci_contains_local(const char* haystack, const char* needle) {
if (!haystack || !needle) return 0;
if (needle[0] == '\0') return 1;
@@ -445,6 +480,323 @@ static cJSON* extract_skill_summary_local(cJSON* event) {
return summary;
}
typedef struct {
char* d_tag;
char* author_pubkey;
char* content;
cJSON* tags;
int kind;
int is_own;
} skill_fetch_result_local_t;
static int g_skill_run_depth_local = 0;
static pthread_mutex_t g_skill_run_depth_mutex_local = PTHREAD_MUTEX_INITIALIZER;
static void free_skill_fetch_result_local(skill_fetch_result_local_t* result) {
if (!result) return;
free(result->d_tag);
free(result->author_pubkey);
free(result->content);
cJSON_Delete(result->tags);
memset(result, 0, sizeof(*result));
}
static int set_error_local(char** out_error, const char* msg) {
if (!out_error) return -1;
*out_error = strdup(msg ? msg : "error");
return (*out_error) ? 0 : -1;
}
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 maybe_append_loaded_skill_instructions_local(cJSON* messages,
const char* tool_name,
const char* tool_result_json) {
if (!messages || !tool_name || strcmp(tool_name, "skill_load") != 0 || !tool_result_json) {
return 0;
}
cJSON* root = cJSON_Parse(tool_result_json);
if (!root || !cJSON_IsObject(root)) {
cJSON_Delete(root);
return 0;
}
cJSON* ok = cJSON_GetObjectItemCaseSensitive(root, "success");
cJSON* type = cJSON_GetObjectItemCaseSensitive(root, "type");
cJSON* instructions = cJSON_GetObjectItemCaseSensitive(root, "instructions");
int should_apply = (ok && cJSON_IsBool(ok) && cJSON_IsTrue(ok) &&
type && cJSON_IsString(type) && type->valuestring &&
strcmp(type->valuestring, "skill_instructions") == 0 &&
instructions && cJSON_IsString(instructions) && instructions->valuestring &&
instructions->valuestring[0] != '\0');
int rc = 0;
if (should_apply) {
rc = append_simple_message_local(messages, "system", instructions->valuestring);
}
cJSON_Delete(root);
return rc;
}
static int is_tool_sandbox_safe_local(const char* tool_name) {
if (!tool_name || tool_name[0] == '\0') return 0;
static const char* safe_tools[] = {
"nostr_query", "nostr_nip05_lookup", "nostr_profile_get",
"nostr_encode", "nostr_decode", "nostr_relay_status",
"nostr_relay_info", "nostr_post", "nostr_react",
"nostr_list_manage", "nostr_dm_send", "nostr_dm_send_nip17",
"skill_list", "skill_search", "skill_get", "skill_view",
"skill_load", "skill_run",
"memory_recall", "my_memory", "memory_short_term_read",
"agent_version", "nostr_pubkey", "nostr_npub",
"tool_list", "trigger_list", "message_current",
"agent_identity", "admin_identity",
NULL
};
for (int i = 0; safe_tools[i] != NULL; i++) {
if (strcmp(tool_name, safe_tools[i]) == 0) return 1;
}
return 0;
}
static char* build_sandboxed_tools_json_local(const tools_context_t* ctx) {
char* full_tools_json = tools_build_openai_schema_json(ctx);
if (!full_tools_json) return NULL;
cJSON* arr = cJSON_Parse(full_tools_json);
free(full_tools_json);
if (!arr || !cJSON_IsArray(arr)) {
cJSON_Delete(arr);
return NULL;
}
for (int i = cJSON_GetArraySize(arr) - 1; i >= 0; i--) {
cJSON* item = cJSON_GetArrayItem(arr, i);
cJSON* fn = item ? cJSON_GetObjectItemCaseSensitive(item, "function") : NULL;
cJSON* name = fn ? cJSON_GetObjectItemCaseSensitive(fn, "name") : NULL;
if (!name || !cJSON_IsString(name) || !name->valuestring || !is_tool_sandbox_safe_local(name->valuestring)) {
cJSON_DeleteItemFromArray(arr, i);
}
}
char* out = cJSON_PrintUnformatted(arr);
cJSON_Delete(arr);
return out;
}
static int fetch_skill_from_events_local(tools_context_t* ctx,
cJSON* events,
const char* d_tag,
const char* pubkey_filter,
skill_fetch_result_local_t* out,
char** out_error) {
if (!ctx || !ctx->cfg || !events || !cJSON_IsArray(events) || !d_tag || !out) {
if (out_error) (void)set_error_local(out_error, "skill lookup failed");
return -1;
}
cJSON* best = NULL;
long long best_created = -1;
int n = cJSON_GetArraySize(events);
for (int i = 0; i < n; i++) {
cJSON* ev = cJSON_GetArrayItem(events, i);
if (!ev || !cJSON_IsObject(ev)) continue;
cJSON* kind = cJSON_GetObjectItemCaseSensitive(ev, "kind");
cJSON* pubkey = cJSON_GetObjectItemCaseSensitive(ev, "pubkey");
cJSON* tags = cJSON_GetObjectItemCaseSensitive(ev, "tags");
if (!kind || !cJSON_IsNumber(kind) || !pubkey || !cJSON_IsString(pubkey) || !pubkey->valuestring || !tags || !cJSON_IsArray(tags)) {
continue;
}
int kind_val = (int)kind->valuedouble;
if (kind_val != 31123 && kind_val != 31124) continue;
if (pubkey_filter && strcmp(pubkey_filter, pubkey->valuestring) != 0) continue;
cJSON* d_val = find_tag_value_string_local(tags, "d");
if (!d_val || !cJSON_IsString(d_val) || !d_val->valuestring || strcmp(d_val->valuestring, d_tag) != 0) continue;
cJSON* created_at = cJSON_GetObjectItemCaseSensitive(ev, "created_at");
long long created = (created_at && cJSON_IsNumber(created_at)) ? (long long)created_at->valuedouble : 0;
if (!best || created > best_created) {
best = ev;
best_created = created;
}
}
if (!best) {
if (out_error) (void)set_error_local(out_error, "skill not found");
return -1;
}
cJSON* kind = cJSON_GetObjectItemCaseSensitive(best, "kind");
cJSON* pubkey = cJSON_GetObjectItemCaseSensitive(best, "pubkey");
cJSON* content = cJSON_GetObjectItemCaseSensitive(best, "content");
cJSON* tags = cJSON_GetObjectItemCaseSensitive(best, "tags");
if (!kind || !cJSON_IsNumber(kind) || !pubkey || !cJSON_IsString(pubkey) || !pubkey->valuestring ||
!content || !cJSON_IsString(content) || !content->valuestring || !tags || !cJSON_IsArray(tags)) {
if (out_error) (void)set_error_local(out_error, "malformed skill event");
return -1;
}
out->kind = (int)kind->valuedouble;
out->d_tag = strdup(d_tag);
out->author_pubkey = strdup(pubkey->valuestring);
out->content = strdup(content->valuestring);
out->tags = cJSON_Duplicate(tags, 1);
out->is_own = (strcmp(pubkey->valuestring, ctx->cfg->keys.public_key_hex) == 0) ? 1 : 0;
if (!out->d_tag || !out->author_pubkey || !out->content || !out->tags) {
free_skill_fetch_result_local(out);
if (out_error) (void)set_error_local(out_error, "allocation failure");
return -1;
}
return 0;
}
static int fetch_skill_by_identifier_local(tools_context_t* ctx,
const char* d_tag,
const char* pubkey,
skill_fetch_result_local_t* out,
char** out_error) {
if (out) memset(out, 0, sizeof(*out));
if (out_error) *out_error = NULL;
if (!ctx || !ctx->cfg || !out || !d_tag || !validate_skill_d_tag_local(d_tag)) {
if (out_error) (void)set_error_local(out_error, "skill identifier is invalid");
return -1;
}
if (pubkey && pubkey[0] != '\0' && !is_hex_string_len_local(pubkey, 64U)) {
if (out_error) (void)set_error_local(out_error, "pubkey must be 64-char hex when provided");
return -1;
}
int own_lookup = (!pubkey || pubkey[0] == '\0' || strcmp(pubkey, ctx->cfg->keys.public_key_hex) == 0);
char* events_json = NULL;
if (own_lookup) {
events_json = nostr_handler_get_self_skill_events_json();
if (!events_json) {
if (out_error) (void)set_error_local(out_error, "self skill cache unavailable");
return -1;
}
} else {
cJSON* filter = cJSON_CreateObject();
cJSON* kinds = cJSON_CreateArray();
cJSON* authors = cJSON_CreateArray();
cJSON* d_tags = cJSON_CreateArray();
if (!filter || !kinds || !authors || !d_tags) {
cJSON_Delete(filter);
cJSON_Delete(kinds);
cJSON_Delete(authors);
cJSON_Delete(d_tags);
if (out_error) (void)set_error_local(out_error, "failed to build external skill filter");
return -1;
}
cJSON_AddItemToArray(kinds, cJSON_CreateNumber(31123));
cJSON_AddItemToArray(kinds, cJSON_CreateNumber(31124));
cJSON_AddItemToArray(authors, cJSON_CreateString(pubkey));
cJSON_AddItemToArray(d_tags, cJSON_CreateString(d_tag));
cJSON_AddItemToObject(filter, "kinds", kinds);
cJSON_AddItemToObject(filter, "authors", authors);
cJSON_AddItemToObject(filter, "#d", d_tags);
cJSON_AddNumberToObject(filter, "limit", 20);
events_json = nostr_handler_query_json(filter, 8000);
cJSON_Delete(filter);
if (!events_json) {
if (out_error) (void)set_error_local(out_error, "external skill query failed");
return -1;
}
}
cJSON* events = cJSON_Parse(events_json);
free(events_json);
if (!events || !cJSON_IsArray(events)) {
cJSON_Delete(events);
if (out_error) (void)set_error_local(out_error, "skill lookup returned invalid JSON");
return -1;
}
int rc = fetch_skill_from_events_local(ctx, events, d_tag, own_lookup ? NULL : pubkey, out, out_error);
cJSON_Delete(events);
return rc;
}
char* execute_skill_create(tools_context_t* ctx, const char* args_json) {
if (!ctx || !ctx->cfg) return json_error_local("tool context unavailable");
@@ -1770,6 +2122,321 @@ char* execute_skill_edit(tools_context_t* ctx, const char* args_json) {
return json;
}
char* execute_skill_run(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* d_tag = cJSON_GetObjectItemCaseSensitive(args, "d_tag");
cJSON* d = cJSON_GetObjectItemCaseSensitive(args, "d");
cJSON* pubkey = cJSON_GetObjectItemCaseSensitive(args, "pubkey");
cJSON* run_args = cJSON_GetObjectItemCaseSensitive(args, "args");
cJSON* input = cJSON_GetObjectItemCaseSensitive(args, "input");
cJSON* sandbox = cJSON_GetObjectItemCaseSensitive(args, "sandbox");
const char* d_tag_value = NULL;
if (d_tag && cJSON_IsString(d_tag) && d_tag->valuestring) {
d_tag_value = d_tag->valuestring;
} else if (d && cJSON_IsString(d) && d->valuestring) {
d_tag_value = d->valuestring;
}
if (!d_tag_value || !validate_skill_d_tag_local(d_tag_value)) {
cJSON_Delete(args);
return json_error_local("skill_run requires valid d_tag");
}
const char* pubkey_value = NULL;
if (pubkey && cJSON_IsString(pubkey) && pubkey->valuestring && pubkey->valuestring[0] != '\0') {
pubkey_value = pubkey->valuestring;
}
const char* user_args = "";
if (run_args && cJSON_IsString(run_args) && run_args->valuestring) {
user_args = run_args->valuestring;
} else if (input && cJSON_IsString(input) && input->valuestring) {
user_args = input->valuestring;
}
int depth_incremented = 0;
pthread_mutex_lock(&g_skill_run_depth_mutex_local);
if (g_skill_run_depth_local >= 3) {
pthread_mutex_unlock(&g_skill_run_depth_mutex_local);
cJSON_Delete(args);
return json_error_local("skill_run max recursion depth exceeded");
}
g_skill_run_depth_local++;
depth_incremented = 1;
pthread_mutex_unlock(&g_skill_run_depth_mutex_local);
skill_fetch_result_local_t skill;
memset(&skill, 0, sizeof(skill));
char* fetch_error = NULL;
char* tools_json = NULL;
char* final_result = NULL;
cJSON* messages = NULL;
char* response_json = NULL;
int turns_run = 0;
llm_config_t old_llm_cfg;
int llm_overridden = 0;
if (fetch_skill_by_identifier_local(ctx, d_tag_value, pubkey_value, &skill, &fetch_error) != 0) {
response_json = json_error_local(fetch_error ? fetch_error : "skill lookup failed");
goto cleanup;
}
int sandbox_enabled = (!skill.is_own) ? 1 : 0;
if (sandbox && cJSON_IsBool(sandbox)) {
sandbox_enabled = cJSON_IsTrue(sandbox) ? 1 : 0;
}
cJSON* llm_tag = find_tag_value_string_local(skill.tags, "llm");
if (llm_tag && cJSON_IsString(llm_tag) && llm_tag->valuestring && llm_tag->valuestring[0] != '\0') {
if (llm_get_config(&old_llm_cfg) == 0) {
llm_config_t next_llm_cfg = old_llm_cfg;
apply_llm_spec_local_to_config(llm_tag->valuestring, &next_llm_cfg);
if (llm_set_config(&next_llm_cfg) == 0) {
llm_overridden = 1;
}
}
}
const char* source = skill.is_own ? "own" : "external";
size_t sys_cap = strlen(skill.d_tag ? skill.d_tag : d_tag_value) +
strlen(skill.content ? skill.content : "") +
strlen(source) + 320U;
char* system_prompt = (char*)malloc(sys_cap);
if (!system_prompt) {
response_json = json_error_local("allocation failure");
goto cleanup;
}
snprintf(system_prompt,
sys_cap,
"Skill execution context:\n"
"- You are executing a specific skill on demand.\n"
"- Follow the skill instructions below precisely.\n"
"- Return a concise, actionable result.\n\n"
"Skill d_tag: %s\n"
"Skill source: %s\n\n"
"Skill instructions:\n%s",
skill.d_tag ? skill.d_tag : d_tag_value,
source,
skill.content ? skill.content : "");
tools_json = sandbox_enabled ? build_sandboxed_tools_json_local(ctx) : tools_build_openai_schema_json(ctx);
if (!tools_json) {
free(system_prompt);
response_json = json_error_local("skill_run failed to build tool schema");
goto cleanup;
}
messages = cJSON_CreateArray();
if (!messages ||
append_simple_message_local(messages, "system", system_prompt) != 0 ||
append_simple_message_local(messages, "user", user_args) != 0) {
free(system_prompt);
response_json = json_error_local("skill_run failed to initialize messages");
goto cleanup;
}
free(system_prompt);
int max_turns = ctx->cfg->tools.trigger_max_turns > 0
? ctx->cfg->tools.trigger_max_turns
: (ctx->cfg->tools.max_turns > 0 ? ctx->cfg->tools.max_turns : 8);
for (int turn = 0; turn < max_turns; turn++) {
turns_run = turn + 1;
char* messages_json = cJSON_PrintUnformatted(messages);
if (!messages_json) {
response_json = json_error_local("skill_run failed to serialize messages");
goto cleanup;
}
llm_response_t resp;
int rc = llm_chat_with_tools_messages(messages_json, tools_json, "auto", &resp);
free(messages_json);
if (rc != 0) {
response_json = json_error_local("skill_run LLM request failed");
goto cleanup;
}
if (resp.tool_call_count <= 0) {
final_result = strdup(resp.content ? resp.content : "");
llm_response_free(&resp);
if (!final_result) {
response_json = json_error_local("allocation failure");
goto cleanup;
}
break;
}
if (append_assistant_tool_calls_message_local(messages, &resp) != 0) {
llm_response_free(&resp);
response_json = json_error_local("skill_run failed to append assistant tool calls");
goto cleanup;
}
for (int i = 0; i < resp.tool_call_count; i++) {
llm_tool_call_t* tc = &resp.tool_calls[i];
char* tool_result = NULL;
if (sandbox_enabled && !is_tool_sandbox_safe_local(tc->name)) {
tool_result = strdup("{\"success\":false,\"error\":\"tool blocked by skill_run sandbox\"}");
} else {
tool_result = tools_execute(ctx, tc->name, tc->arguments_json);
}
if (!tool_result) {
tool_result = strdup("{\"success\":false,\"error\":\"tool execution failed\"}");
}
if (append_tool_result_message_local(messages,
tc->id ? tc->id : "",
tool_result ? tool_result : "{\"success\":false,\"error\":\"tool execution failed\"}") != 0) {
free(tool_result);
llm_response_free(&resp);
response_json = json_error_local("skill_run failed to append tool result");
goto cleanup;
}
if (maybe_append_loaded_skill_instructions_local(messages, tc->name, tool_result) != 0) {
free(tool_result);
llm_response_free(&resp);
response_json = json_error_local("skill_run failed to apply loaded skill instructions");
goto cleanup;
}
free(tool_result);
}
llm_response_free(&resp);
}
if (!final_result) {
final_result = strdup("");
if (!final_result) {
response_json = json_error_local("allocation failure");
goto cleanup;
}
}
cJSON* out = cJSON_CreateObject();
if (!out) {
response_json = NULL;
goto cleanup;
}
cJSON_AddBoolToObject(out, "success", 1);
cJSON_AddStringToObject(out, "d_tag", skill.d_tag ? skill.d_tag : d_tag_value);
cJSON_AddStringToObject(out, "source", source);
cJSON_AddBoolToObject(out, "sandboxed", sandbox_enabled ? 1 : 0);
cJSON_AddNumberToObject(out, "turns", turns_run);
cJSON_AddStringToObject(out, "result", final_result);
response_json = cJSON_PrintUnformatted(out);
cJSON_Delete(out);
cleanup:
free(final_result);
cJSON_Delete(messages);
free(tools_json);
free(fetch_error);
free_skill_fetch_result_local(&skill);
cJSON_Delete(args);
if (llm_overridden) {
(void)llm_set_config(&old_llm_cfg);
}
if (depth_incremented) {
pthread_mutex_lock(&g_skill_run_depth_mutex_local);
g_skill_run_depth_local--;
if (g_skill_run_depth_local < 0) g_skill_run_depth_local = 0;
pthread_mutex_unlock(&g_skill_run_depth_mutex_local);
}
return response_json ? response_json : json_error_local("skill_run failed");
}
char* execute_skill_load(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* d_tag = cJSON_GetObjectItemCaseSensitive(args, "d_tag");
cJSON* d = cJSON_GetObjectItemCaseSensitive(args, "d");
cJSON* pubkey = cJSON_GetObjectItemCaseSensitive(args, "pubkey");
const char* d_tag_value = NULL;
if (d_tag && cJSON_IsString(d_tag) && d_tag->valuestring) {
d_tag_value = d_tag->valuestring;
} else if (d && cJSON_IsString(d) && d->valuestring) {
d_tag_value = d->valuestring;
}
if (!d_tag_value || !validate_skill_d_tag_local(d_tag_value)) {
cJSON_Delete(args);
return json_error_local("skill_load requires valid d_tag");
}
const char* pubkey_value = NULL;
if (pubkey && cJSON_IsString(pubkey) && pubkey->valuestring && pubkey->valuestring[0] != '\0') {
pubkey_value = pubkey->valuestring;
}
skill_fetch_result_local_t skill;
memset(&skill, 0, sizeof(skill));
char* fetch_error = NULL;
if (fetch_skill_by_identifier_local(ctx, d_tag_value, pubkey_value, &skill, &fetch_error) != 0) {
char* err = json_error_local(fetch_error ? fetch_error : "skill lookup failed");
free(fetch_error);
cJSON_Delete(args);
return err;
}
const char* content = skill.content ? skill.content : "";
size_t instructions_cap = strlen(content) + 256U;
char* instructions = (char*)malloc(instructions_cap);
if (!instructions) {
free_skill_fetch_result_local(&skill);
cJSON_Delete(args);
return json_error_local("allocation failure");
}
snprintf(instructions,
instructions_cap,
"SKILL INSTRUCTIONS — Follow these for the remainder of this conversation:\n\n%s\n\nEND SKILL INSTRUCTIONS",
content);
cJSON* out = cJSON_CreateObject();
if (!out) {
free(instructions);
free_skill_fetch_result_local(&skill);
cJSON_Delete(args);
return NULL;
}
cJSON_AddBoolToObject(out, "success", 1);
cJSON_AddStringToObject(out, "type", "skill_instructions");
cJSON_AddStringToObject(out, "d_tag", skill.d_tag ? skill.d_tag : d_tag_value);
cJSON_AddStringToObject(out, "source", skill.is_own ? "own" : "external");
cJSON_AddStringToObject(out, "instructions", instructions);
cJSON_AddStringToObject(out,
"note",
"The above instructions should be followed for subsequent reasoning in this conversation.");
char* json = cJSON_PrintUnformatted(out);
cJSON_Delete(out);
free(instructions);
free_skill_fetch_result_local(&skill);
free(fetch_error);
cJSON_Delete(args);
return json;
}
char* execute_skill_refresh(tools_context_t* ctx, const char* args_json) {
(void)args_json;
if (!ctx || !ctx->cfg) return json_error_local("tool context unavailable");
+6
View File
@@ -149,6 +149,12 @@ char* tools_execute_legacy(tools_context_t* ctx, const char* tool_name, const ch
if (strcmp(tool_name, "skill_search") == 0) {
return execute_skill_search(args_json);
}
if (strcmp(tool_name, "skill_run") == 0 || strcmp(tool_name, "run") == 0) {
return execute_skill_run(ctx, args_json);
}
if (strcmp(tool_name, "skill_load") == 0) {
return execute_skill_load(ctx, args_json);
}
if (strcmp(tool_name, "skill_edit") == 0) {
return execute_skill_edit(ctx, args_json);
}
+2
View File
@@ -65,6 +65,8 @@ char* execute_skill_remove(tools_context_t* ctx, const char* args_json);
char* execute_skill_edit(tools_context_t* ctx, const char* args_json);
char* execute_skill_refresh(tools_context_t* ctx, const char* args_json);
char* execute_skill_search(const char* args_json);
char* execute_skill_run(tools_context_t* ctx, const char* args_json);
char* execute_skill_load(tools_context_t* ctx, const char* args_json);
char* execute_memory_save(tools_context_t* ctx, const char* args_json);
char* execute_memory_recall(tools_context_t* ctx, const char* args_json);
+83
View File
@@ -1117,6 +1117,89 @@ char* tools_build_openai_schema_json_legacy(const tools_context_t* ctx) {
cJSON_AddItemToObject(t26, "function", t26_fn);
cJSON_AddItemToArray(tools, t26);
cJSON* t26a = cJSON_CreateObject();
cJSON* t26a_fn = cJSON_CreateObject();
cJSON* t26a_params = cJSON_CreateObject();
cJSON* t26a_props = cJSON_CreateObject();
cJSON* t26a_required = cJSON_CreateArray();
cJSON_AddStringToObject(t26a, "type", "function");
cJSON_AddStringToObject(t26a_fn, "name", "skill_run");
cJSON_AddStringToObject(t26a_fn, "description", "Execute a skill one-shot in an isolated context with optional sandboxing. Use d_tag for own skills or pubkey+d_tag for external skills.");
cJSON_AddStringToObject(t26a_params, "type", "object");
cJSON_AddItemToObject(t26a_params, "properties", t26a_props);
cJSON_AddItemToObject(t26a_params, "required", t26a_required);
cJSON* p_skill_run_d_tag = cJSON_CreateObject();
cJSON_AddStringToObject(p_skill_run_d_tag, "type", "string");
cJSON_AddStringToObject(p_skill_run_d_tag, "description", "Skill d tag identifier");
cJSON_AddItemToObject(t26a_props, "d_tag", p_skill_run_d_tag);
cJSON* p_skill_run_d = cJSON_CreateObject();
cJSON_AddStringToObject(p_skill_run_d, "type", "string");
cJSON_AddStringToObject(p_skill_run_d, "description", "Compatibility alias for d_tag");
cJSON_AddItemToObject(t26a_props, "d", p_skill_run_d);
cJSON* p_skill_run_pubkey = cJSON_CreateObject();
cJSON_AddStringToObject(p_skill_run_pubkey, "type", "string");
cJSON_AddStringToObject(p_skill_run_pubkey, "description", "Optional author pubkey for external skills");
cJSON_AddItemToObject(t26a_props, "pubkey", p_skill_run_pubkey);
cJSON* p_skill_run_args = cJSON_CreateObject();
cJSON_AddStringToObject(p_skill_run_args, "type", "string");
cJSON_AddStringToObject(p_skill_run_args, "description", "Arguments/context to pass to the skill");
cJSON_AddItemToObject(t26a_props, "args", p_skill_run_args);
cJSON* p_skill_run_input = cJSON_CreateObject();
cJSON_AddStringToObject(p_skill_run_input, "type", "string");
cJSON_AddStringToObject(p_skill_run_input, "description", "Slash shorthand fallback for args");
cJSON_AddItemToObject(t26a_props, "input", p_skill_run_input);
cJSON* p_skill_run_sandbox = cJSON_CreateObject();
cJSON_AddStringToObject(p_skill_run_sandbox, "type", "boolean");
cJSON_AddStringToObject(p_skill_run_sandbox, "description", "Override sandbox behavior (default on for external skills)");
cJSON_AddItemToObject(t26a_props, "sandbox", p_skill_run_sandbox);
cJSON_AddItemToArray(t26a_required, cJSON_CreateString("d_tag"));
cJSON_AddItemToObject(t26a_fn, "parameters", t26a_params);
cJSON_AddItemToObject(t26a, "function", t26a_fn);
cJSON_AddItemToArray(tools, t26a);
cJSON* t26b = cJSON_CreateObject();
cJSON* t26b_fn = cJSON_CreateObject();
cJSON* t26b_params = cJSON_CreateObject();
cJSON* t26b_props = cJSON_CreateObject();
cJSON* t26b_required = cJSON_CreateArray();
cJSON_AddStringToObject(t26b, "type", "function");
cJSON_AddStringToObject(t26b_fn, "name", "skill_load");
cJSON_AddStringToObject(t26b_fn, "description", "Load a skill's instructions into the current conversation context.");
cJSON_AddStringToObject(t26b_params, "type", "object");
cJSON_AddItemToObject(t26b_params, "properties", t26b_props);
cJSON_AddItemToObject(t26b_params, "required", t26b_required);
cJSON* p_skill_load_d_tag = cJSON_CreateObject();
cJSON_AddStringToObject(p_skill_load_d_tag, "type", "string");
cJSON_AddStringToObject(p_skill_load_d_tag, "description", "Skill d tag identifier");
cJSON_AddItemToObject(t26b_props, "d_tag", p_skill_load_d_tag);
cJSON* p_skill_load_d = cJSON_CreateObject();
cJSON_AddStringToObject(p_skill_load_d, "type", "string");
cJSON_AddStringToObject(p_skill_load_d, "description", "Compatibility alias for d_tag");
cJSON_AddItemToObject(t26b_props, "d", p_skill_load_d);
cJSON* p_skill_load_pubkey = cJSON_CreateObject();
cJSON_AddStringToObject(p_skill_load_pubkey, "type", "string");
cJSON_AddStringToObject(p_skill_load_pubkey, "description", "Optional author pubkey for external skills");
cJSON_AddItemToObject(t26b_props, "pubkey", p_skill_load_pubkey);
cJSON_AddItemToArray(t26b_required, cJSON_CreateString("d_tag"));
cJSON_AddItemToObject(t26b_fn, "parameters", t26b_params);
cJSON_AddItemToObject(t26b, "function", t26b_fn);
cJSON_AddItemToArray(tools, t26b);
cJSON* t27 = cJSON_CreateObject();
cJSON* t27_fn = cJSON_CreateObject();
cJSON* t27_params = cJSON_CreateObject();