v0.2.30 - Implemented NIP-17 bootstrap/discovery (kind 10050) and added seal/rumor pubkey verification
This commit is contained in:
@@ -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.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
|
||||
- Publishes configured startup events per relay as each relay becomes connected
|
||||
|
||||
+35
-15004
File diff suppressed because one or more lines are too long
@@ -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",
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user