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. 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.** **Active build — this project is barely working. Experiment at your own risk.**
> Last release update: v0.2.29 — Fix self-skill EOSE startup deadlock by adding per-relay timeout fallback in nostr core relay pool > Last release update: v0.2.30 — Implemented NIP-17 bootstrap/discovery (kind 10050) and added seal/rumor pubkey verification
- Connects to configured relays with auto-reconnect and relay state transition logging - Connects to configured relays with auto-reconnect and relay state transition logging
- Publishes configured startup events per relay as each relay becomes connected - 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"] ["r", "wss://relay.primal.net"]
] ]
}, },
{
"kind": 10050,
"content": "",
"tags": [
["relay", "wss://relay.damus.io"],
["relay", "wss://relay.primal.net"]
]
},
{ {
"kind": 31124, "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", "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"] ["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, "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\"})}}", "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 ## 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: The `nostr_core_lib` already has full NIP-17/NIP-59 support including:
- `nostr_nip17_create_chat_event()` — create kind 14 rumor - `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 ### 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 Implementation detail: kind `10050` reuses the same relay set as kind `10002`/current config relays, with `relay` tags as required by NIP-17.
{
"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.
### 8. Update startup DM to use auto-routing ### 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 ## 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; 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) { static char* local_json_error(const char* msg) {
cJSON* out = cJSON_CreateObject(); cJSON* out = cJSON_CreateObject();
if (!out) return NULL; if (!out) return NULL;
@@ -292,6 +324,51 @@ static char* build_slash_args_json(const char* raw_args) {
return out; 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, ...) { static int append_textf_local(char** buf, size_t* cap, size_t* used, const char* fmt, ...) {
if (!buf || !cap || !used || !fmt) return -1; 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; char* result_json = NULL;
if (raw_args && (strcmp(raw_args, "-h") == 0 || strcmp(raw_args, "--help") == 0)) { 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) { } else if (strcmp(tool_name, "help") == 0) {
if (!raw_args || raw_args[0] == '\0') { if (!raw_args || raw_args[0] == '\0') {
result_json = build_slash_help_all_json(); 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++; i++;
} }
help_tool[i] = '\0'; 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 { } 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) { if (!args_json) {
result_json = local_json_error("failed to parse slash args"); result_json = local_json_error("failed to parse slash args");
} else { } 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); free(args_json);
} }
} }
@@ -1864,6 +1952,17 @@ void agent_on_trigger(const char* skill_d_tag,
return; 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); 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."); (void)nostr_handler_send_dm_auto(sender_pubkey_hex, "Failed to append tool result.");
return; 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); free(tool_result);
} }
+4 -1
View File
@@ -12,11 +12,13 @@
#define DIDACTYL_STARTUP_KIND_PROFILE 0 #define DIDACTYL_STARTUP_KIND_PROFILE 0
#define DIDACTYL_STARTUP_KIND_CONTACTS 3 #define DIDACTYL_STARTUP_KIND_CONTACTS 3
#define DIDACTYL_STARTUP_KIND_RELAYS 10002 #define DIDACTYL_STARTUP_KIND_RELAYS 10002
#define DIDACTYL_STARTUP_KIND_DM_RELAYS 10050
static const int DIDACTYL_DEFAULT_STARTUP_EVENT_KINDS[] = { static const int DIDACTYL_DEFAULT_STARTUP_EVENT_KINDS[] = {
DIDACTYL_STARTUP_KIND_PROFILE, DIDACTYL_STARTUP_KIND_PROFILE,
DIDACTYL_STARTUP_KIND_CONTACTS, 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]))) #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_CONTENT ""
#define DIDACTYL_DEFAULT_KIND3_TAGS_JSON_TEMPLATE "[[\"p\",\"%s\"]]" #define DIDACTYL_DEFAULT_KIND3_TAGS_JSON_TEMPLATE "[[\"p\",\"%s\"]]"
#define DIDACTYL_DEFAULT_KIND10002_CONTENT "" #define DIDACTYL_DEFAULT_KIND10002_CONTENT ""
#define DIDACTYL_DEFAULT_KIND10050_CONTENT ""
static const char* DIDACTYL_DEFAULT_RELAYS[] = { static const char* DIDACTYL_DEFAULT_RELAYS[] = {
"wss://relay.damus.io", "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; 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) { static int is_space_char_local(char c) {
return c == ' ' || c == '\t' || c == '\n' || c == '\r'; 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; 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, ...) { static int append_textf_http(char** buf, size_t* cap, size_t* used, const char* fmt, ...) {
if (!buf || !cap || !used || !fmt) return -1; if (!buf || !cap || !used || !fmt) return -1;
@@ -853,7 +930,8 @@ static char* execute_slash_command_http(const char* message) {
char* result_json = NULL; char* result_json = NULL;
if (raw_args && (strcmp(raw_args, "-h") == 0 || strcmp(raw_args, "--help") == 0)) { 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) { } else if (strcmp(tool_name, "help") == 0) {
if (!raw_args || raw_args[0] == '\0') { if (!raw_args || raw_args[0] == '\0') {
result_json = build_slash_help_all_json_local(); result_json = build_slash_help_all_json_local();
@@ -865,14 +943,24 @@ static char* execute_slash_command_http(const char* message) {
i++; i++;
} }
help_tool[i] = '\0'; 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 { } 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) { if (!args_json) {
result_json = json_error_string_local("failed to parse slash args"); result_json = json_error_string_local("failed to parse slash args");
} else { } 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); free(args_json);
} }
} }
@@ -1004,6 +1092,15 @@ static cJSON* run_prompt_with_tools_convo(cJSON* convo,
final_response = strdup("Failed to append tool result."); final_response = strdup("Failed to append tool result.");
goto done; 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); free(tool_result);
} }
+2 -2
View File
@@ -12,8 +12,8 @@
// Using DIDACTYL_ prefix to avoid conflicts with nostr_core_lib VERSION macros // Using DIDACTYL_ prefix to avoid conflicts with nostr_core_lib VERSION macros
#define DIDACTYL_VERSION_MAJOR 0 #define DIDACTYL_VERSION_MAJOR 0
#define DIDACTYL_VERSION_MINOR 2 #define DIDACTYL_VERSION_MINOR 2
#define DIDACTYL_VERSION_PATCH 29 #define DIDACTYL_VERSION_PATCH 30
#define DIDACTYL_VERSION "v0.2.29" #define DIDACTYL_VERSION "v0.2.30"
// Agent metadata // Agent metadata
#define DIDACTYL_NAME "Didactyl" #define DIDACTYL_NAME "Didactyl"
+30 -3
View File
@@ -346,8 +346,12 @@ static int set_default_relays(didactyl_config_t* cfg) {
return 0; return 0;
} }
static int build_kind10002_tags_json(const didactyl_config_t* cfg, char** out_tags_json) { static int build_relay_tags_json_with_key(const didactyl_config_t* cfg,
if (!cfg || !out_tags_json || cfg->relay_count <= 0 || !cfg->relays) return -1; 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; *out_tags_json = NULL;
cJSON* tags = cJSON_CreateArray(); 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); cJSON_Delete(tags);
return -1; 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(tag, cJSON_CreateString(cfg->relays[i]));
cJSON_AddItemToArray(tags, tag); cJSON_AddItemToArray(tags, tag);
} }
@@ -372,6 +376,14 @@ static int build_kind10002_tags_json(const didactyl_config_t* cfg, char** out_ta
return 0; 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) { 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) { if (!tags_json || !out || out_size == 0) {
return -1; return -1;
@@ -537,6 +549,21 @@ static int apply_default_startup_events(didactyl_config_t* cfg, const char* agen
} }
continue; 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; return 0;
+667
View File
@@ -12,6 +12,7 @@
#include "../nostr_handler.h" #include "../nostr_handler.h"
#include "../trigger_manager.h" #include "../trigger_manager.h"
#include "../agent.h" #include "../agent.h"
#include "../llm.h"
static char* json_error_local(const char* msg) { static char* json_error_local(const char* msg) {
cJSON* root = cJSON_CreateObject(); cJSON* root = cJSON_CreateObject();
@@ -254,6 +255,40 @@ static int validate_skill_d_tag_local(const char* d_tag) {
return 1; 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) { static int ci_contains_local(const char* haystack, const char* needle) {
if (!haystack || !needle) return 0; if (!haystack || !needle) return 0;
if (needle[0] == '\0') return 1; if (needle[0] == '\0') return 1;
@@ -445,6 +480,323 @@ static cJSON* extract_skill_summary_local(cJSON* event) {
return summary; 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) { char* execute_skill_create(tools_context_t* ctx, const char* args_json) {
if (!ctx || !ctx->cfg) return json_error_local("tool context unavailable"); 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; 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) { char* execute_skill_refresh(tools_context_t* ctx, const char* args_json) {
(void)args_json; (void)args_json;
if (!ctx || !ctx->cfg) return json_error_local("tool context unavailable"); 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) { if (strcmp(tool_name, "skill_search") == 0) {
return execute_skill_search(args_json); 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) { if (strcmp(tool_name, "skill_edit") == 0) {
return execute_skill_edit(ctx, args_json); 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_edit(tools_context_t* ctx, const char* args_json);
char* execute_skill_refresh(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_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_save(tools_context_t* ctx, const char* args_json);
char* execute_memory_recall(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_AddItemToObject(t26, "function", t26_fn);
cJSON_AddItemToArray(tools, t26); 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 = cJSON_CreateObject();
cJSON* t27_fn = cJSON_CreateObject(); cJSON* t27_fn = cJSON_CreateObject();
cJSON* t27_params = cJSON_CreateObject(); cJSON* t27_params = cJSON_CreateObject();