v0.2.52 - Fix trigger manager bypassing skill adoption list — cron/webhook triggers now only fire for adopted and enabled skills

This commit is contained in:
Didactyl User
2026-07-10 19:13:19 -04:00
parent 4430c36bcb
commit 3ed3733b76
4 changed files with 260 additions and 8 deletions

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.51 ## Current Status — v0.2.52
**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.51Implement n_signer integration through Phase 4: signer config/CLI/wizard, signer-backed signing/encryption/auth/cashu/blossom paths, nsigner_unix e2e validation, and signer health observability > Last release update: v0.2.52Fix trigger manager bypassing skill adoption list — cron/webhook triggers now only fire for adopted and enabled skills
- 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

View File

@@ -0,0 +1,77 @@
# Fix: Trigger Manager Bypasses Skill Adoption List
## Problem
`trigger_manager_load_from_skills()` and `trigger_manager_reconcile_from_relays()`
activate cron/webhook/subscription triggers for **every** self-authored skill event
(kind 31123/31124) that has trigger metadata, without checking whether the skill is
on the kind 10123 adoption list.
This means a skill that was published but never adopted (or was un-adopted) will
still have its cron trigger firing.
Contrast: `agent.c:refresh_adopted_skills_cache_if_needed()` properly fetches the
kind 10123 list, parses `"a"` tags, and only loads adopted skills — and also checks
the `enabled` tag. The trigger manager was written as a parallel system that skips
both gates.
## Root Cause
- `trigger_manager.c:939``trigger_manager_load_from_skills()`: no reference to
kind 10123 or adoption anywhere in the function. Calls `trigger_manager_add()`
unconditionally.
- `trigger_manager.c:592``trigger_manager_reconcile_from_relays()`: same bug.
Currently disabled (`enable_periodic_reconcile = 0` at line 1575) but should be
fixed before re-enabling.
- Neither function checks the `enabled` tag either, unlike
`trigger_manager_load_from_startup_events()` (line 1078) which does check it.
## Fix
### 1. Add a static helper: `load_adopted_d_tags()`
Add a static function in `trigger_manager.c` that:
- Fetches kind 10123 events via `nostr_handler_get_self_events_by_kind_json(10123)`
- Selects the latest event by `created_at` (same logic as agent.c)
- Parses `"a"` tags using a local `parse_skill_address_tag_local()` equivalent
- Populates a fixed-size array of adopted d_tag strings
- Returns the count
Since both call sites only deal with self-authored skills, matching on d_tag
alone is sufficient (the pubkey in the "a" tag will be the agent's own).
### 2. Add `enabled` tag check
Both functions should read the `enabled` tag (defaulting to `"true"`) and skip
skills where `enabled` is `false` or `0`, matching the pattern already used in
`trigger_manager_load_from_startup_events()`.
### 3. Gate `trigger_manager_load_from_skills()`
Before calling `trigger_manager_add()`, check:
1. The skill's d_tag is in the adopted set
2. The skill's `enabled` tag is not false/0
If the adoption list is empty or missing, log a warning and load nothing (do not
fall back to loading all skills — that's the bug).
### 4. Gate `trigger_manager_reconcile_from_relays()`
Apply the same adoption + enabled checks. This ensures the code is correct when
`enable_periodic_reconcile` is eventually flipped back to 1.
### 5. Pass `is_enabled` to `trigger_manager_add()`
Currently both functions pass `1` (hardcoded enabled) as the `enabled` parameter.
Change to pass the actual `is_enabled` value derived from the `enabled` tag.
## Files Changed
- `src/trigger_manager.c` — add helper, fix both functions
## Risk
Low. The fix adds a filter before `trigger_manager_add()`. If the adoption list
is present and correct, behavior is unchanged for adopted skills. If the adoption
list is missing, triggers simply won't load (which is the correct behavior —
nothing should fire if nothing is adopted).

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 51 #define DIDACTYL_VERSION_PATCH 52
#define DIDACTYL_VERSION "v0.2.51" #define DIDACTYL_VERSION "v0.2.52"
// Agent metadata // Agent metadata
#define DIDACTYL_NAME "Didactyl" #define DIDACTYL_NAME "Didactyl"

View File

@@ -589,6 +589,126 @@ static void parse_trigger_runtime_tags(cJSON* tags,
} }
} }
/*
* Load the set of adopted skill d_tags from the kind 10123 adoption list.
*
* Fetches the agent's latest kind 10123 event from cache, parses the "a" tags,
* and populates out_d_tags with the d_tag portion of each adopted skill address.
*
* Since both trigger load paths only deal with self-authored skills, matching on
* d_tag alone is sufficient — the pubkey in the "a" tag will be the agent's own.
*
* Returns the number of adopted d_tags, or -1 on error.
*/
#define TRIGGER_ADOPTED_D_TAGS_MAX 64
static int load_adopted_d_tags(trigger_manager_t* mgr,
char out_d_tags[][65],
int max_d_tags) {
if (!mgr || !mgr->cfg || !out_d_tags || max_d_tags <= 0) {
return -1;
}
int count = 0;
char* adoption_json = nostr_handler_get_self_events_by_kind_json(10123);
if (!adoption_json) {
DEBUG_WARN("[didactyl] trigger adoption check: no kind 10123 event in cache");
return 0;
}
cJSON* adoption_events = cJSON_Parse(adoption_json);
free(adoption_json);
if (!adoption_events || !cJSON_IsArray(adoption_events) ||
cJSON_GetArraySize(adoption_events) <= 0) {
cJSON_Delete(adoption_events);
DEBUG_WARN("[didactyl] trigger adoption check: kind 10123 parse failed or empty");
return 0;
}
/* Find the latest adoption list event by created_at */
cJSON* list_event = NULL;
double list_event_created_at = -1.0;
int adoption_n = cJSON_GetArraySize(adoption_events);
for (int ai = 0; ai < adoption_n; ai++) {
cJSON* ev = cJSON_GetArrayItem(adoption_events, ai);
if (!ev || !cJSON_IsObject(ev)) {
continue;
}
cJSON* ev_pubkey = cJSON_GetObjectItemCaseSensitive(ev, "pubkey");
if (!ev_pubkey || !cJSON_IsString(ev_pubkey) || !ev_pubkey->valuestring ||
strcmp(ev_pubkey->valuestring, mgr->cfg->keys.public_key_hex) != 0) {
continue;
}
cJSON* created_at = cJSON_GetObjectItemCaseSensitive(ev, "created_at");
double created = (created_at && cJSON_IsNumber(created_at)) ? created_at->valuedouble : 0.0;
if (!list_event || created > list_event_created_at) {
list_event = ev;
list_event_created_at = created;
}
}
cJSON* list_tags = list_event ? cJSON_GetObjectItemCaseSensitive(list_event, "tags") : NULL;
if (!list_tags || !cJSON_IsArray(list_tags)) {
cJSON_Delete(adoption_events);
DEBUG_INFO("[didactyl] trigger adoption check: no tags in adoption list event");
return 0;
}
int tn = cJSON_GetArraySize(list_tags);
for (int i = 0; i < tn && count < max_d_tags; i++) {
cJSON* tag = cJSON_GetArrayItem(list_tags, i);
if (!tag || !cJSON_IsArray(tag)) {
continue;
}
cJSON* key = cJSON_GetArrayItem(tag, 0);
cJSON* val = cJSON_GetArrayItem(tag, 1);
if (!key || !val || !cJSON_IsString(key) || !cJSON_IsString(val) ||
!key->valuestring || !val->valuestring ||
strcmp(key->valuestring, "a") != 0) {
continue;
}
/* Parse "kind:pubkey:d_tag" — we only need the d_tag (3rd field) */
const char* p1 = strchr(val->valuestring, ':');
if (!p1) continue;
const char* p2 = strchr(p1 + 1, ':');
if (!p2) continue;
const char* d_tag_start = p2 + 1;
size_t d_tag_len = strlen(d_tag_start);
if (d_tag_len == 0 || d_tag_len >= 65U) {
continue;
}
snprintf(out_d_tags[count], 65, "%s", d_tag_start);
count++;
}
cJSON_Delete(adoption_events);
DEBUG_INFO("[didactyl] trigger adoption check: %d adopted skill(s) found", count);
return count;
}
/*
* Check whether d_tag appears in the adopted_d_tags array.
*/
static int is_d_tag_adopted(const char* d_tag,
char adopted_d_tags[][65],
int adopted_count) {
if (!d_tag || d_tag[0] == '\0' || !adopted_d_tags || adopted_count <= 0) {
return 0;
}
for (int i = 0; i < adopted_count; i++) {
if (strcmp(adopted_d_tags[i], d_tag) == 0) {
return 1;
}
}
return 0;
}
static int trigger_manager_reconcile_from_relays(trigger_manager_t* mgr) { static int trigger_manager_reconcile_from_relays(trigger_manager_t* mgr) {
if (!mgr || !mgr->cfg) { if (!mgr || !mgr->cfg) {
return -1; return -1;
@@ -628,6 +748,18 @@ static int trigger_manager_reconcile_from_relays(trigger_manager_t* mgr) {
int considered = 0; int considered = 0;
int loaded = 0; int loaded = 0;
int skipped_not_adopted = 0;
int skipped_disabled = 0;
/* Load adopted d_tags from kind 10123 adoption list */
char adopted_d_tags[TRIGGER_ADOPTED_D_TAGS_MAX][65];
memset(adopted_d_tags, 0, sizeof(adopted_d_tags));
int adopted_count = load_adopted_d_tags(mgr, adopted_d_tags, TRIGGER_ADOPTED_D_TAGS_MAX);
if (adopted_count <= 0) {
DEBUG_WARN("[didactyl] trigger reconcile: no adopted skills found — skipping all trigger registration");
cJSON_Delete(events);
return 0;
}
int event_count = cJSON_GetArraySize(events); int event_count = cJSON_GetArraySize(events);
for (int i = 0; i < event_count; i++) { for (int i = 0; i < event_count; i++) {
@@ -642,17 +774,25 @@ static int trigger_manager_reconcile_from_relays(trigger_manager_t* mgr) {
cJSON* trigger = find_tag_value_string(tags, "trigger"); cJSON* trigger = find_tag_value_string(tags, "trigger");
cJSON* filter_j = find_tag_value_string(tags, "filter"); cJSON* filter_j = find_tag_value_string(tags, "filter");
cJSON* action = find_tag_value_string(tags, "action"); cJSON* action = find_tag_value_string(tags, "action");
cJSON* enabled = find_tag_value_string(tags, "enabled");
const char* d_tag = (d && cJSON_IsString(d) && d->valuestring) ? d->valuestring : NULL; const char* d_tag = (d && cJSON_IsString(d) && d->valuestring) ? d->valuestring : NULL;
const char* trigger_s = (trigger && cJSON_IsString(trigger) && trigger->valuestring) ? trigger->valuestring : NULL; const char* trigger_s = (trigger && cJSON_IsString(trigger) && trigger->valuestring) ? trigger->valuestring : NULL;
const char* filter_s = (filter_j && cJSON_IsString(filter_j) && filter_j->valuestring) ? filter_j->valuestring : NULL; const char* filter_s = (filter_j && cJSON_IsString(filter_j) && filter_j->valuestring) ? filter_j->valuestring : NULL;
const char* action_s = (action && cJSON_IsString(action) && action->valuestring) ? action->valuestring : "llm"; const char* action_s = (action && cJSON_IsString(action) && action->valuestring) ? action->valuestring : "llm";
const char* enabled_s = (enabled && cJSON_IsString(enabled) && enabled->valuestring) ? enabled->valuestring : "true";
considered++; considered++;
if (!d_tag || d_tag[0] == '\0') { if (!d_tag || d_tag[0] == '\0') {
continue; continue;
} }
/* Adoption gate: only register triggers for adopted skills */
if (!is_d_tag_adopted(d_tag, adopted_d_tags, adopted_count)) {
skipped_not_adopted++;
continue;
}
int trigger_supported = trigger_s && int trigger_supported = trigger_s &&
(strcmp(trigger_s, "nostr-subscription") == 0 || (strcmp(trigger_s, "nostr-subscription") == 0 ||
strcmp(trigger_s, "webhook") == 0 || strcmp(trigger_s, "webhook") == 0 ||
@@ -663,6 +803,13 @@ static int trigger_manager_reconcile_from_relays(trigger_manager_t* mgr) {
continue; continue;
} }
/* Enabled gate: skip skills explicitly disabled */
int is_enabled = (strcmp(enabled_s, "false") == 0 || strcmp(enabled_s, "0") == 0) ? 0 : 1;
if (!is_enabled) {
skipped_disabled++;
continue;
}
if (strcmp(action_s, "template") == 0) { if (strcmp(action_s, "template") == 0) {
DEBUG_WARN("[didactyl] trigger action template is deprecated; forcing llm for d_tag=%s", d_tag); DEBUG_WARN("[didactyl] trigger action template is deprecated; forcing llm for d_tag=%s", d_tag);
} }
@@ -691,7 +838,7 @@ static int trigger_manager_reconcile_from_relays(trigger_manager_t* mgr) {
filter_s, filter_s,
TRIGGER_ACTION_LLM, TRIGGER_ACTION_LLM,
trigger_s, trigger_s,
1, is_enabled,
llm_s, llm_s,
tools_s, tools_s,
has_max_tokens, has_max_tokens,
@@ -705,7 +852,8 @@ static int trigger_manager_reconcile_from_relays(trigger_manager_t* mgr) {
} }
cJSON_Delete(events); cJSON_Delete(events);
DEBUG_INFO("[didactyl] trigger reconcile complete: loaded=%d considered=%d", loaded, considered); DEBUG_INFO("[didactyl] trigger reconcile complete: loaded=%d considered=%d skipped_not_adopted=%d skipped_disabled=%d",
loaded, considered, skipped_not_adopted, skipped_disabled);
return 0; return 0;
} }
@@ -943,8 +1091,19 @@ int trigger_manager_load_from_skills(trigger_manager_t* mgr) {
int loaded = 0; int loaded = 0;
int considered = 0; int considered = 0;
int skipped_not_adopted = 0;
int skipped_disabled = 0;
const int kinds[2] = {31123, 31124}; const int kinds[2] = {31123, 31124};
/* Load adopted d_tags from kind 10123 adoption list */
char adopted_d_tags[TRIGGER_ADOPTED_D_TAGS_MAX][65];
memset(adopted_d_tags, 0, sizeof(adopted_d_tags));
int adopted_count = load_adopted_d_tags(mgr, adopted_d_tags, TRIGGER_ADOPTED_D_TAGS_MAX);
if (adopted_count <= 0) {
DEBUG_WARN("[didactyl] trigger load from skills: no adopted skills found — skipping all trigger registration");
return 0;
}
for (int k = 0; k < 2; k++) { for (int k = 0; k < 2; k++) {
char* skill_json = nostr_handler_get_self_events_by_kind_json(kinds[k]); char* skill_json = nostr_handler_get_self_events_by_kind_json(kinds[k]);
if (!skill_json) { if (!skill_json) {
@@ -971,17 +1130,25 @@ int trigger_manager_load_from_skills(trigger_manager_t* mgr) {
cJSON* trigger = find_tag_value_string(tags, "trigger"); cJSON* trigger = find_tag_value_string(tags, "trigger");
cJSON* filter = find_tag_value_string(tags, "filter"); cJSON* filter = find_tag_value_string(tags, "filter");
cJSON* action = find_tag_value_string(tags, "action"); cJSON* action = find_tag_value_string(tags, "action");
cJSON* enabled = find_tag_value_string(tags, "enabled");
const char* d_tag = (d && cJSON_IsString(d) && d->valuestring) ? d->valuestring : NULL; const char* d_tag = (d && cJSON_IsString(d) && d->valuestring) ? d->valuestring : NULL;
const char* trigger_s = (trigger && cJSON_IsString(trigger) && trigger->valuestring) ? trigger->valuestring : NULL; const char* trigger_s = (trigger && cJSON_IsString(trigger) && trigger->valuestring) ? trigger->valuestring : NULL;
const char* filter_s = (filter && cJSON_IsString(filter) && filter->valuestring) ? filter->valuestring : NULL; const char* filter_s = (filter && cJSON_IsString(filter) && filter->valuestring) ? filter->valuestring : NULL;
const char* action_s = (action && cJSON_IsString(action) && action->valuestring) ? action->valuestring : "llm"; const char* action_s = (action && cJSON_IsString(action) && action->valuestring) ? action->valuestring : "llm";
const char* enabled_s = (enabled && cJSON_IsString(enabled) && enabled->valuestring) ? enabled->valuestring : "true";
considered++; considered++;
if (!d_tag || d_tag[0] == '\0') { if (!d_tag || d_tag[0] == '\0') {
continue; continue;
} }
/* Adoption gate: only register triggers for adopted skills */
if (!is_d_tag_adopted(d_tag, adopted_d_tags, adopted_count)) {
skipped_not_adopted++;
continue;
}
int trigger_supported = trigger_s && int trigger_supported = trigger_s &&
(strcmp(trigger_s, "nostr-subscription") == 0 || (strcmp(trigger_s, "nostr-subscription") == 0 ||
strcmp(trigger_s, "webhook") == 0 || strcmp(trigger_s, "webhook") == 0 ||
@@ -992,6 +1159,13 @@ int trigger_manager_load_from_skills(trigger_manager_t* mgr) {
continue; continue;
} }
/* Enabled gate: skip skills explicitly disabled */
int is_enabled = (strcmp(enabled_s, "false") == 0 || strcmp(enabled_s, "0") == 0) ? 0 : 1;
if (!is_enabled) {
skipped_disabled++;
continue;
}
if (strcmp(action_s, "template") == 0) { if (strcmp(action_s, "template") == 0) {
DEBUG_WARN("[didactyl] trigger action template is deprecated; forcing llm for d_tag=%s", d_tag); DEBUG_WARN("[didactyl] trigger action template is deprecated; forcing llm for d_tag=%s", d_tag);
} }
@@ -1020,7 +1194,7 @@ int trigger_manager_load_from_skills(trigger_manager_t* mgr) {
filter_s, filter_s,
TRIGGER_ACTION_LLM, TRIGGER_ACTION_LLM,
trigger_s, trigger_s,
1, is_enabled,
llm_s, llm_s,
tools_s, tools_s,
has_max_tokens, has_max_tokens,
@@ -1036,7 +1210,8 @@ int trigger_manager_load_from_skills(trigger_manager_t* mgr) {
cJSON_Delete(skill_events); cJSON_Delete(skill_events);
} }
DEBUG_INFO("[didactyl] trigger manager loaded %d trigger(s) from self skills (considered=%d)", loaded, considered); DEBUG_INFO("[didactyl] trigger manager loaded %d trigger(s) from self skills (considered=%d skipped_not_adopted=%d skipped_disabled=%d)",
loaded, considered, skipped_not_adopted, skipped_disabled);
return 0; return 0;
} }