diff --git a/.gitignore b/.gitignore index d4b201d..f62f819 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ /c-relay/ /nips/ /config.json/ +test_keys.txt # Build artifacts /build/ /didactyl diff --git a/README.md b/README.md index 456b7cf..40c1188 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Because all identity, communication, and memory live on Nostr, the agent is **po **Skills are the new apps.** Agents learn capabilities through skills — public Nostr events that any agent can discover, adopt, and share. There is no app store, no gatekeeper, no approval process. If someone publishes a useful skill, your agent can find it through your web of trust and start using it. Popularity is measured by adoption, not by a rating algorithm. The best skills spread because agents actually use them. -## Current Status +## Current Status — v0.0.8 **Active build — relay-aware autonomous agent with tool-use and Nostr-native startup memory.** @@ -22,15 +22,16 @@ Because all identity, communication, and memory live on Nostr, the agent is **po - Verifies Nostr event signatures before processing inbound messages - Applies privilege tiers: ADMIN (tools), WoT (chat-only), STRANGER (configurable canned reply or ignore) - Subscribes to admin context kinds (`0`,`3`,`10002`,`1`) for WoT + contextual awareness -- Builds LLM context from system prompt + startup events + admin DM history + admin recent notes +- Builds LLM context from system prompt + admin identity (kind 0/10002) + startup events + admin DM history + admin recent notes - Supports tool-calling loop with configurable max turns and local safety limits +- Deduplicates inbound messages via event-ID cache and FNV-1a fingerprint debounce window - Appends every outbound LLM context payload to [`context.log`](context.log) ## Quick Start ### Download binary (recommended) -1. Download the latest release binary from GitLab: +1. Download the latest release binary from Gitea: 2. Make it executable and run it: ```bash @@ -148,8 +149,8 @@ Send an encrypted DM to the agent pubkey using any Nostr client (Damus, Amethyst │ Didactyl │ │ │ │ ┌──────────┐ ┌──────────┐ ┌────────────┐ │ -│ │ config │ │ skills │ │ agent │ │ -│ │ loader │ │ loader │ │ loop │ │ +│ │ config │ │ context │ │ agent │ │ +│ │ loader │ │ loader │ │ loop │ │ │ └────┬─────┘ └────┬─────┘ └─────┬──────┘ │ │ │ │ │ │ │ ▼ ▼ ▼ │ @@ -221,10 +222,11 @@ Didactyl builds tier-aware context: - **ADMIN** request context order: 1. Soul message from kind `31120` (or fallback default) - 2. Startup events memory block (`kinds/content/tags` snapshot) - 3. Last 12 decrypted DM turns between admin and agent - 4. Recent admin kind `1` notes (from configured admin-context subscription) - 5. Current user message + 2. Admin identity context — admin pubkey hex, kind `0` profile, kind `10002` relay list + 3. Startup events memory block (`kinds/content/tags` snapshot) + 4. Last 12 decrypted DM turns between admin and agent + 5. Recent admin kind `1` notes (from configured admin-context subscription) + 6. Current user message - **WoT** request context: Soul + WoT chat-only instruction + current user message (no tools) - **STRANGER**: no LLM call when configured to reply statically @@ -251,8 +253,9 @@ Execution entrypoint: [`tools_execute()`](src/tools.c:434). ├── Makefile # Build system ├── build_static.sh # Preferred final build validation ├── src/ -│ ├── main.c # Entry point, args (--config/--debug), lifecycle +│ ├── main.c / .h # Entry point, args (--config/--debug), lifecycle, version │ ├── config.c / .h # JSON config parsing, key decode, startup events +│ ├── context.c / .h # File loader utility (reads file into malloc'd string) │ ├── agent.c / .h # Context assembly, tool loop, DM response flow │ ├── tools.c / .h # LLM tool schema and tool execution │ ├── llm.c / .h # LLM HTTP API client (OpenAI-compatible) @@ -260,7 +263,8 @@ Execution entrypoint: [`tools_execute()`](src/tools.c:434). │ └── debug.c / .h # Runtime log levels/macros ├── plans/ # Architecture and planning documents │ ├── didactyl_mvp.md -│ └── didactyl_agentic.md +│ ├── didactyl_agentic.md +│ └── security_and_admin_context.md └── README.md ``` @@ -287,6 +291,10 @@ All dependencies are statically linked into the binary at build time. No system - [x] Context payload logging to [`context.log`](context.log) - [x] Skill kind definitions (`31120` Soul, `31123` Public Skill, `31124` Private Skill) - [x] Skill adoption list (`10123`) for WoT-driven discovery +- [x] Signature verification on all inbound events +- [x] Privilege tiers — ADMIN (tools), WoT (chat-only), STRANGER (canned reply/ignore) +- [x] Admin context subscription (kind 0, 3, 10002, 1) with WoT contact extraction +- [x] Message deduplication (event-ID cache + FNV-1a fingerprint debounce) - [ ] Runtime skill loading from adopted `31123` events on relays - [ ] Skill discovery CLI/tool (query WoT adoption lists) - [ ] Upgrade to NIP-17 gift-wrapped DMs diff --git a/src/agent.c b/src/agent.c index 13157fb..780a5ea 100644 --- a/src/agent.c +++ b/src/agent.c @@ -277,6 +277,59 @@ static int append_startup_events_context(cJSON* messages) { return rc; } +static int append_admin_identity_context(cJSON* messages) { + if (!messages || !g_cfg) { + return -1; + } + + char admin_header[256]; + snprintf(admin_header, + sizeof(admin_header), + "This is your administrator! Admin pubkey (hex): %s", + g_cfg->admin.pubkey ? g_cfg->admin.pubkey : "unknown"); + if (append_simple_message(messages, "system", admin_header) != 0) { + return -1; + } + + char* kind0 = nostr_handler_get_admin_kind0_context(); + if (kind0) { + const char* prefix = "Administrator kind 0 profile content (JSON): "; + size_t n = strlen(prefix) + strlen(kind0) + 1U; + char* payload = (char*)malloc(n); + if (!payload) { + free(kind0); + return -1; + } + snprintf(payload, n, "%s%s", prefix, kind0); + int rc = append_simple_message(messages, "system", payload); + free(payload); + free(kind0); + if (rc != 0) { + return -1; + } + } + + char* kind10002 = nostr_handler_get_admin_kind10002_context(); + if (kind10002) { + const char* prefix = "Administrator kind 10002 relay-list content (JSON): "; + size_t n = strlen(prefix) + strlen(kind10002) + 1U; + char* payload = (char*)malloc(n); + if (!payload) { + free(kind10002); + return -1; + } + snprintf(payload, n, "%s%s", prefix, kind10002); + int rc = append_simple_message(messages, "system", payload); + free(payload); + free(kind10002); + if (rc != 0) { + return -1; + } + } + + return 0; +} + static int append_recent_admin_dm_history(cJSON* messages, const char* current_message) { if (!messages || !g_cfg) { return -1; @@ -509,6 +562,7 @@ void agent_on_message(const char* sender_pubkey_hex, } if (append_simple_message(messages, "system", g_system_context) != 0 || + append_admin_identity_context(messages) != 0 || append_startup_events_context(messages) != 0 || append_recent_admin_dm_history(messages, message) != 0) { cJSON_Delete(messages); diff --git a/src/main.h b/src/main.h index 3601fb9..6e20161 100644 --- a/src/main.h +++ b/src/main.h @@ -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 0 -#define DIDACTYL_VERSION_PATCH 8 -#define DIDACTYL_VERSION "v0.0.8" +#define DIDACTYL_VERSION_PATCH 9 +#define DIDACTYL_VERSION "v0.0.9" // Agent metadata #define DIDACTYL_NAME "Didactyl" diff --git a/src/nostr_handler.c b/src/nostr_handler.c index f8b89a1..d5ad403 100644 --- a/src/nostr_handler.c +++ b/src/nostr_handler.c @@ -1024,6 +1024,28 @@ const char* nostr_handler_get_system_context(void) { return g_system_context; } +char* nostr_handler_get_admin_kind0_context(void) { + if (!g_cfg || !g_cfg->admin_context.enabled || !g_cfg->admin_context.track_kind_0) { + return NULL; + } + + pthread_mutex_lock(&g_admin_ctx_mutex); + char* out = g_admin_kind0_json ? strdup(g_admin_kind0_json) : NULL; + pthread_mutex_unlock(&g_admin_ctx_mutex); + return out; +} + +char* nostr_handler_get_admin_kind10002_context(void) { + if (!g_cfg || !g_cfg->admin_context.enabled || !g_cfg->admin_context.track_kind_10002) { + return NULL; + } + + pthread_mutex_lock(&g_admin_ctx_mutex); + char* out = g_admin_kind10002_json ? strdup(g_admin_kind10002_json) : NULL; + pthread_mutex_unlock(&g_admin_ctx_mutex); + return out; +} + char* nostr_handler_get_admin_kind1_notes_context(void) { if (!g_cfg || !g_cfg->admin_context.enabled || !g_cfg->admin_context.track_kind_1) { return NULL; diff --git a/src/nostr_handler.h b/src/nostr_handler.h index f1f838f..95e3414 100644 --- a/src/nostr_handler.h +++ b/src/nostr_handler.h @@ -24,6 +24,8 @@ char* nostr_handler_query_json(cJSON* filter, int timeout_ms); int nostr_handler_poll(int timeout_ms); int nostr_handler_reconcile_startup_events(void); const char* nostr_handler_get_system_context(void); +char* nostr_handler_get_admin_kind0_context(void); +char* nostr_handler_get_admin_kind10002_context(void); char* nostr_handler_get_admin_kind1_notes_context(void); int nostr_handler_is_wot_contact(const char* pubkey_hex); void nostr_handler_cleanup(void);