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:
@@ -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.51
|
||||
## Current Status — v0.2.52
|
||||
|
||||
**Active build — this project is barely working. Experiment at your own risk.**
|
||||
|
||||
> Last release update: v0.2.51 — Implement 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.52 — Fix 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
|
||||
- Publishes configured startup events per relay as each relay becomes connected
|
||||
|
||||
77
plans/fix_trigger_adoption_gate.md
Normal file
77
plans/fix_trigger_adoption_gate.md
Normal 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).
|
||||
@@ -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 51
|
||||
#define DIDACTYL_VERSION "v0.2.51"
|
||||
#define DIDACTYL_VERSION_PATCH 52
|
||||
#define DIDACTYL_VERSION "v0.2.52"
|
||||
|
||||
// Agent metadata
|
||||
#define DIDACTYL_NAME "Didactyl"
|
||||
|
||||
@@ -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) {
|
||||
if (!mgr || !mgr->cfg) {
|
||||
return -1;
|
||||
@@ -628,6 +748,18 @@ static int trigger_manager_reconcile_from_relays(trigger_manager_t* mgr) {
|
||||
|
||||
int considered = 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);
|
||||
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* filter_j = find_tag_value_string(tags, "filter");
|
||||
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* 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* 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++;
|
||||
if (!d_tag || d_tag[0] == '\0') {
|
||||
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 &&
|
||||
(strcmp(trigger_s, "nostr-subscription") == 0 ||
|
||||
strcmp(trigger_s, "webhook") == 0 ||
|
||||
@@ -663,6 +803,13 @@ static int trigger_manager_reconcile_from_relays(trigger_manager_t* mgr) {
|
||||
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) {
|
||||
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,
|
||||
TRIGGER_ACTION_LLM,
|
||||
trigger_s,
|
||||
1,
|
||||
is_enabled,
|
||||
llm_s,
|
||||
tools_s,
|
||||
has_max_tokens,
|
||||
@@ -705,7 +852,8 @@ static int trigger_manager_reconcile_from_relays(trigger_manager_t* mgr) {
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -943,8 +1091,19 @@ int trigger_manager_load_from_skills(trigger_manager_t* mgr) {
|
||||
|
||||
int loaded = 0;
|
||||
int considered = 0;
|
||||
int skipped_not_adopted = 0;
|
||||
int skipped_disabled = 0;
|
||||
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++) {
|
||||
char* skill_json = nostr_handler_get_self_events_by_kind_json(kinds[k]);
|
||||
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* filter = find_tag_value_string(tags, "filter");
|
||||
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* 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* 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++;
|
||||
if (!d_tag || d_tag[0] == '\0') {
|
||||
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 &&
|
||||
(strcmp(trigger_s, "nostr-subscription") == 0 ||
|
||||
strcmp(trigger_s, "webhook") == 0 ||
|
||||
@@ -992,6 +1159,13 @@ int trigger_manager_load_from_skills(trigger_manager_t* mgr) {
|
||||
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) {
|
||||
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,
|
||||
TRIGGER_ACTION_LLM,
|
||||
trigger_s,
|
||||
1,
|
||||
is_enabled,
|
||||
llm_s,
|
||||
tools_s,
|
||||
has_max_tokens,
|
||||
@@ -1036,7 +1210,8 @@ int trigger_manager_load_from_skills(trigger_manager_t* mgr) {
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user