diff --git a/Dockerfile.alpine-musl b/Dockerfile.alpine-musl
index c16190e..9854f46 100644
--- a/Dockerfile.alpine-musl
+++ b/Dockerfile.alpine-musl
@@ -97,7 +97,7 @@ RUN if [ "$DEBUG_BUILD" = "true" ]; then \
src/main.c src/config.c src/context.c src/llm.c \
src/nostr_handler.c src/agent.c src/tools/tools_common.c src/tools/tools_schema.c src/tools/tools_dispatch.c \
src/tools/tool_agent.c src/tools/tool_meta.c src/tools/tool_model.c \
- src/tools/tool_nostr_query.c src/tools/tool_nostr_identity.c src/tools/tool_nostr_social.c \
+ src/tools/tool_nostr_query.c src/tools/tool_nostr_my_events.c src/tools/tool_nostr_identity.c src/tools/tool_nostr_social.c \
src/tools/tool_nostr_relay.c src/tools/tool_nostr_dm.c src/tools/tool_admin.c \
src/tools/tool_task.c src/tools/tool_nostr_list.c src/tools/tool_local.c \
src/tools/tool_skill.c src/tools/tool_nostr_post.c src/tools/tool_memory.c src/tools/tool_config.c src/trigger_manager.c \
diff --git a/Makefile b/Makefile
index a3ffb97..b09946b 100644
--- a/Makefile
+++ b/Makefile
@@ -18,6 +18,7 @@ SRCS = \
$(SRC_DIR)/tools/tool_meta.c \
$(SRC_DIR)/tools/tool_model.c \
$(SRC_DIR)/tools/tool_nostr_query.c \
+ $(SRC_DIR)/tools/tool_nostr_my_events.c \
$(SRC_DIR)/tools/tool_nostr_identity.c \
$(SRC_DIR)/tools/tool_nostr_social.c \
$(SRC_DIR)/tools/tool_nostr_relay.c \
diff --git a/README.md b/README.md
index f79c8c0..6acd716 100644
--- a/README.md
+++ b/README.md
@@ -55,11 +55,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.0.72
+## Current Status — v0.0.73
**Active build — this project is barely working. Experiment at your own risk.**
-> Last release update: v0.0.72 — Refined wizard framing and menu hotkey display; diagnosed hardcoded Didactyl fallback causing name mismatch
+> Last release update: v0.0.73 — Refined Nostr handler and tool dispatch/schema updates for relay and my-events flows
- Connects to configured relays with auto-reconnect and relay state transition logging
- Publishes configured startup events per relay as each relay becomes connected
diff --git a/context_template.recovered.md b/context_template.recovered.md
new file mode 100644
index 0000000..11c708a
--- /dev/null
+++ b/context_template.recovered.md
@@ -0,0 +1,56 @@
+# Context Template
+
+```yaml
+- section: admin_identity
+ role: system
+ content: |
+ ## Administrator Identity (source: config.admin.pubkey)
+
+ This is your administrator! Admin pubkey (hex): {{admin_pubkey}}
+
+- section: admin_profile
+ role: system
+ content: |
+ ## Administrator Kind 0 Profile (source: nostr kind 0)
+
+ Administrator kind 0 profile content (JSON): {{admin_kind0_json}}
+ provider:
+ anthropic: |
+
+ {{admin_kind0_json}}
+
+
+- section: admin_relay_list
+ role: system
+ content: |
+ ## Administrator Relay List (source: nostr kind 10002)
+
+ Administrator kind 10002 relay-list content (JSON): {{admin_kind10002_json}}
+
+- section: startup_events
+ role: system
+ content: |
+ ## Startup Events Memory (source: config.startup_events)
+
+ Startup events memory (kinds/content/tags): {{startup_events_json}}
+
+- section: adopted_skills
+ role: system
+ content: |
+ {{adopted_skills_content}}
+
+- section: agent_tasks
+ role: system
+ content: |
+ {{tasks_content}}
+
+- section: dm_history
+ role: expand
+ limit: 12
+
+- section: admin_notes
+ role: system
+ content: |
+ ## Administrator Recent Notes (source: nostr kind 1)
+
+ {{admin_notes_content}}
\ No newline at end of file
diff --git a/plans/server_installation_guide.md b/plans/server_installation_guide.md
new file mode 100644
index 0000000..61e7f48
--- /dev/null
+++ b/plans/server_installation_guide.md
@@ -0,0 +1,459 @@
+# Didactyl Server Installation Guide — systemd + Dedicated User Model
+
+## Overview
+
+This document describes how to install Didactyl on a Linux server as a dedicated system user, managed by systemd, with scoped sudo privileges for server maintenance tasks.
+
+The mental model: **Didactyl is a person on your server.** It gets its own home directory, its own login identity, and explicit permission to manage the services you delegate to it — nothing more.
+
+## Architecture
+
+```mermaid
+graph TD
+ subgraph Internet
+ RELAYS[Nostr Relays
wss://relay.damus.io
wss://relay.primal.net]
+ LLM_API[LLM Provider API
OpenAI / PPQ / Ollama]
+ end
+
+ subgraph Server
+ subgraph "systemd"
+ SVC[didactyl.service
User=didactyl
Group=didactyl]
+ end
+
+ subgraph "/home/didactyl/"
+ BIN[didactyl binary]
+ CFG[genesis.jsonc
mode 600]
+ LOGS[context.log.md
didactyl.log]
+ end
+
+ subgraph "Privilege Boundary"
+ SUDOERS[/etc/sudoers.d/didactyl
Whitelisted commands only]
+ end
+
+ subgraph "Unprivileged Commands"
+ MON[free / df / uptime
ps / cat /proc/*
ss -tlnp / top]
+ end
+
+ subgraph "Privileged Commands via sudo"
+ SYSD[systemctl status/restart/start/stop
journalctl
daemon-reload]
+ end
+ end
+
+ ADMIN[Administrator
via Nostr DM] -->|NIP-04 encrypted| RELAYS
+ RELAYS <-->|WebSocket| SVC
+ SVC --> BIN
+ BIN <-->|HTTPS| LLM_API
+ BIN -->|popen as didactyl| MON
+ BIN -->|sudo via popen| SUDOERS
+ SUDOERS --> SYSD
+
+ style CFG fill:#f66,stroke:#333,color:#fff
+ style SUDOERS fill:#ff9,stroke:#333
+ style SVC fill:#9f9,stroke:#333
+```
+
+## Security Model
+
+### Privilege Tiers
+
+Didactyl enforces three privilege tiers for inbound Nostr messages (see `src/config.h` security_config_t):
+
+| Tier | Who | Can use tools? | Can chat? |
+|------|-----|---------------|-----------|
+| **ADMIN** | Your npub (config `admin.pubkey`) | ✅ Yes | ✅ Yes |
+| **WoT** | Contacts in your follow list | ❌ No (configurable) | ✅ Yes |
+| **Stranger** | Everyone else | ❌ No | Canned response or ignored |
+
+Only the ADMIN tier can trigger `local_shell_exec` — the tool that runs commands on the server.
+
+### OS-Level Privilege Separation
+
+The `local_shell_exec` tool in `src/tools/tool_local.c` calls `popen()` directly. Whatever the process user can do, the agent can do. This is why we:
+
+1. Run as a **dedicated unprivileged user** (`didactyl`)
+2. Grant **specific sudo permissions** via `/etc/sudoers.d/didactyl`
+3. Use **systemd sandboxing** directives to limit filesystem access
+
+### What the Agent Cannot Do
+
+With the configuration described in this guide:
+
+- ❌ Write anywhere outside `/home/didactyl/`
+- ❌ Run arbitrary root commands not in the sudoers whitelist
+- ❌ Install or remove packages (unless explicitly whitelisted)
+- ❌ Modify system configuration files in `/etc/`
+- ❌ Access other users' home directories (with proper home directory permissions)
+- ❌ Accept tool commands from anyone except the ADMIN pubkey
+
+---
+
+## Installation Steps
+
+### Prerequisites
+
+- A Linux server with systemd (Debian/Ubuntu, RHEL/Fedora, Arch, etc.)
+- Network access to Nostr relays and your LLM provider
+- The Didactyl static binary (download from releases or build with `./build_static.sh`)
+- A Nostr keypair for the agent (nsec)
+- Your admin Nostr keypair (npub)
+- An LLM API key
+
+### Step 1: Create the Didactyl User
+
+```bash
+# Create a regular user with a home directory and bash shell
+sudo useradd -m -s /bin/bash -c "Didactyl Nostr Agent" didactyl
+```
+
+**Why bash and not nologin?** The `local_shell_exec` tool runs commands via `sh -lc` (login shell). A real shell ensures PATH, locale, and environment are properly initialized. The agent needs to "log in" to do its job.
+
+**Why a real home directory?** The agent needs a workspace for:
+- Its binary and config
+- Log files (`context.log.md`, `didactyl.log`)
+- Any files it creates during shell operations
+- The `working_directory` for the shell tool
+
+### Step 2: Install the Binary and Config
+
+```bash
+# Copy the static binary
+sudo cp didactyl_static_x86_64 /home/didactyl/didactyl
+sudo chmod 755 /home/didactyl/didactyl
+
+# Copy and configure genesis.jsonc
+sudo cp genesis.jsonc /home/didactyl/genesis.jsonc
+
+# CRITICAL: Lock down permissions on the config file (contains nsec private key)
+sudo chmod 600 /home/didactyl/genesis.jsonc
+
+# Set ownership
+sudo chown -R didactyl:didactyl /home/didactyl/
+
+# Protect the home directory from other users
+sudo chmod 750 /home/didactyl/
+```
+
+### Step 3: Configure genesis.jsonc
+
+Edit `/home/didactyl/genesis.jsonc` with the agent's identity, your admin pubkey, LLM credentials, and the shell working directory:
+
+```jsonc
+{
+ "key": {
+ "nsec": "nsec1..." // Agent's private key
+ },
+
+ "admin": {
+ "pubkey": "npub1..." // YOUR public key
+ },
+
+ "dm_protocol": "nip04",
+
+ "llm": {
+ "provider": "openai",
+ "api_key": "sk-...",
+ "model": "gpt-4o-mini",
+ "base_url": "https://api.openai.com/v1",
+ "max_tokens": 512,
+ "temperature": 0.7
+ },
+
+ "tools": {
+ "enabled": true,
+ "max_turns": 8,
+ "shell": {
+ "enabled": true,
+ "timeout_seconds": 60, // systemctl can be slow
+ "max_output_bytes": 131072, // journalctl output can be large
+ "working_directory": "/home/didactyl"
+ }
+ },
+
+ "api": {
+ "enabled": true,
+ "port": 8484,
+ "bind_address": "127.0.0.1" // Localhost only — do NOT expose
+ },
+
+ "startup_events": [
+ {
+ "kind": 10002,
+ "content": "",
+ "tags": [
+ ["r", "wss://relay.damus.io"],
+ ["r", "wss://relay.primal.net"]
+ ]
+ }
+ ]
+}
+```
+
+**Key settings for server maintenance:**
+
+| Setting | Value | Why |
+|---------|-------|-----|
+| `shell.timeout_seconds` | `60` | `systemctl` and `journalctl` can take time |
+| `shell.max_output_bytes` | `131072` (128KB) | Log output can be verbose |
+| `shell.working_directory` | `/home/didactyl` | Agent's home — safe default CWD |
+| `api.bind_address` | `127.0.0.1` | Never expose the admin API to the network |
+
+### Step 4: Configure sudo Privileges
+
+```bash
+sudo visudo -f /etc/sudoers.d/didactyl
+```
+
+Add the following (adjust to your needs):
+
+```sudoers
+# /etc/sudoers.d/didactyl
+# Didactyl agent — scoped system maintenance privileges
+
+# Service management
+didactyl ALL=(ALL) NOPASSWD: /usr/bin/systemctl status *
+didactyl ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart *
+didactyl ALL=(ALL) NOPASSWD: /usr/bin/systemctl start *
+didactyl ALL=(ALL) NOPASSWD: /usr/bin/systemctl stop *
+didactyl ALL=(ALL) NOPASSWD: /usr/bin/systemctl enable *
+didactyl ALL=(ALL) NOPASSWD: /usr/bin/systemctl disable *
+didactyl ALL=(ALL) NOPASSWD: /usr/bin/systemctl is-active *
+didactyl ALL=(ALL) NOPASSWD: /usr/bin/systemctl is-enabled *
+didactyl ALL=(ALL) NOPASSWD: /usr/bin/systemctl list-units *
+didactyl ALL=(ALL) NOPASSWD: /usr/bin/systemctl daemon-reload
+
+# Log inspection
+didactyl ALL=(ALL) NOPASSWD: /usr/bin/journalctl *
+
+# Network diagnostics (if needed)
+didactyl ALL=(ALL) NOPASSWD: /usr/sbin/lsof *
+```
+
+**Commands that do NOT need sudo** (the didactyl user can run these directly):
+
+- `free -h` — memory usage
+- `df -h` — disk usage
+- `uptime` — load averages
+- `top -bn1` — process snapshot
+- `ps aux` — process list
+- `cat /proc/cpuinfo` — CPU info
+- `cat /proc/meminfo` — memory info
+- `ss -tlnp` — listening ports (as unprivileged user, shows own processes)
+- `uname -a` — kernel info
+- `w` — who is logged in
+
+### Step 5: Create the systemd Unit File
+
+```bash
+sudo tee /etc/systemd/system/didactyl.service << 'EOF'
+[Unit]
+Description=Didactyl Sovereign Nostr Agent
+After=network-online.target
+Wants=network-online.target
+
+[Service]
+Type=simple
+User=didactyl
+Group=didactyl
+WorkingDirectory=/home/didactyl
+
+ExecStart=/home/didactyl/didactyl --config /home/didactyl/genesis.jsonc --debug 3
+
+# --- Privilege & Sandbox ---
+# Must be false — sudo needs to escalate privileges
+NoNewPrivileges=false
+
+# Protect filesystem: read-only except for agent home
+ProtectSystem=strict
+ReadWritePaths=/home/didactyl
+
+# Must be false — the agent's home IS its workspace
+ProtectHome=false
+
+# Isolate /tmp
+PrivateTmp=yes
+
+# --- Environment ---
+Environment=SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt
+
+# --- Restart Policy ---
+Restart=on-failure
+RestartSec=10
+
+# --- Logging ---
+StandardOutput=journal
+StandardError=journal
+SyslogIdentifier=didactyl
+
+[Install]
+WantedBy=multi-user.target
+EOF
+```
+
+**Important systemd notes:**
+
+| Directive | Value | Reason |
+|-----------|-------|--------|
+| `NoNewPrivileges` | `false` | Required for `sudo` to work from `popen()` |
+| `ProtectSystem` | `strict` | Makes `/usr`, `/boot`, `/etc` read-only |
+| `ReadWritePaths` | `/home/didactyl` | Whitelist the agent's home for writes |
+| `ProtectHome` | `false` | The agent lives in `/home/` — can't protect it from itself |
+| `PrivateTmp` | `yes` | Isolates `/tmp` so other processes can't snoop |
+| `SSL_CERT_FILE` | path to CA bundle | Required for TLS connections to relays and LLM API |
+
+### Step 6: Enable and Start
+
+```bash
+# Reload systemd to pick up the new unit
+sudo systemctl daemon-reload
+
+# Enable auto-start on boot
+sudo systemctl enable didactyl
+
+# Start the agent
+sudo systemctl start didactyl
+
+# Verify it's running
+sudo systemctl status didactyl
+
+# Watch logs in real-time
+sudo journalctl -u didactyl -f
+```
+
+### Step 7: Verify the Agent is Working
+
+1. **Check systemd status:**
+ ```bash
+ sudo systemctl status didactyl
+ ```
+ Should show `active (running)`.
+
+2. **Check logs for relay connections:**
+ ```bash
+ sudo journalctl -u didactyl --no-pager -n 50
+ ```
+ Look for relay connection messages and "EOSE" (End of Stored Events).
+
+3. **Send a test DM via Nostr:**
+ From your admin Nostr client, send an encrypted DM to the agent's npub:
+ ```
+ What is the server uptime?
+ ```
+ The agent should call `local_shell_exec` with `uptime` and reply with the result.
+
+4. **Test sudo access:**
+ ```
+ What services are running? Use systemctl list-units --type=service --state=running
+ ```
+
+5. **Check the local API (from the server itself):**
+ ```bash
+ curl http://127.0.0.1:8484/api/context
+ ```
+
+---
+
+## Updating the Agent
+
+To update Didactyl to a new version:
+
+```bash
+# Stop the service
+sudo systemctl stop didactyl
+
+# Replace the binary
+sudo cp didactyl_static_x86_64_new /home/didactyl/didactyl
+sudo chown didactyl:didactyl /home/didactyl/didactyl
+sudo chmod 755 /home/didactyl/didactyl
+
+# Start the service
+sudo systemctl start didactyl
+```
+
+The agent's identity, skills, and memory live on Nostr — replacing the binary doesn't lose any state.
+
+---
+
+## Expanding Privileges
+
+The sudoers file is the single control point for what the agent can do with elevated privileges. To grant additional capabilities:
+
+```bash
+sudo visudo -f /etc/sudoers.d/didactyl
+```
+
+**Examples of additional privileges you might add:**
+
+```sudoers
+# Package management (careful!)
+didactyl ALL=(ALL) NOPASSWD: /usr/bin/apt update
+didactyl ALL=(ALL) NOPASSWD: /usr/bin/apt install *
+didactyl ALL=(ALL) NOPASSWD: /usr/bin/apt upgrade -y
+
+# Docker management
+didactyl ALL=(ALL) NOPASSWD: /usr/bin/docker ps *
+didactyl ALL=(ALL) NOPASSWD: /usr/bin/docker restart *
+didactyl ALL=(ALL) NOPASSWD: /usr/bin/docker logs *
+
+# Firewall inspection
+didactyl ALL=(ALL) NOPASSWD: /usr/sbin/ufw status *
+didactyl ALL=(ALL) NOPASSWD: /usr/sbin/iptables -L *
+
+# Nginx/Apache config test
+didactyl ALL=(ALL) NOPASSWD: /usr/sbin/nginx -t
+didactyl ALL=(ALL) NOPASSWD: /usr/sbin/apachectl configtest
+```
+
+---
+
+## Customizing the Agent's Personality for Server Maintenance
+
+The agent's behavior is defined by its default skill content in `genesis.jsonc` (the `default_skill.content` field). For a server maintenance role, you should customize the soul/skill to include instructions like:
+
+- What services it's responsible for monitoring
+- How frequently to check (via triggered skills)
+- What constitutes an alert-worthy condition
+- How to format status reports
+- Which `sudo` commands are available to it
+- Escalation procedures (when to alert you vs. auto-fix)
+
+This is a skill/prompt engineering task that can be done after the base installation is working.
+
+---
+
+## Quick Reference
+
+### File Locations
+
+| File | Path | Permissions |
+|------|------|-------------|
+| Binary | `/home/didactyl/didactyl` | `755 didactyl:didactyl` |
+| Config | `/home/didactyl/genesis.jsonc` | `600 didactyl:didactyl` |
+| Home directory | `/home/didactyl/` | `750 didactyl:didactyl` |
+| systemd unit | `/etc/systemd/system/didactyl.service` | `644 root:root` |
+| sudoers | `/etc/sudoers.d/didactyl` | `440 root:root` |
+
+### Common Commands
+
+```bash
+# Service management
+sudo systemctl start didactyl
+sudo systemctl stop didactyl
+sudo systemctl restart didactyl
+sudo systemctl status didactyl
+
+# Logs
+sudo journalctl -u didactyl -f # follow live
+sudo journalctl -u didactyl --since today
+sudo journalctl -u didactyl -n 100 # last 100 lines
+
+# Check agent's local API
+curl http://127.0.0.1:8484/api/context
+
+# Edit sudo permissions
+sudo visudo -f /etc/sudoers.d/didactyl
+
+# Edit config (stop service first)
+sudo systemctl stop didactyl
+sudo -u didactyl nano /home/didactyl/genesis.jsonc
+sudo systemctl start didactyl
+```
diff --git a/src/agent.c b/src/agent.c
index 7b2cd00..8646960 100644
--- a/src/agent.c
+++ b/src/agent.c
@@ -468,30 +468,97 @@ static char* build_slash_help_all_json(void) {
cJSON* skill_root = skill_list_json ? cJSON_Parse(skill_list_json) : NULL;
cJSON* skills = skill_root ? cJSON_GetObjectItemCaseSensitive(skill_root, "skills") : NULL;
- if (!skills || !cJSON_IsArray(skills) || cJSON_GetArraySize(skills) <= 0) {
- (void)append_textf_local(&out, &cap, &used, "- (none)\n");
- } else {
+ int available_count = 0;
+ if (skills && cJSON_IsArray(skills) && cJSON_GetArraySize(skills) > 0) {
int sn = cJSON_GetArraySize(skills);
for (int i = 0; i < sn; i++) {
cJSON* row = cJSON_GetArrayItem(skills, i);
+ cJSON* owner = row ? cJSON_GetObjectItemCaseSensitive(row, "owner") : NULL;
cJSON* d_tag = row ? cJSON_GetObjectItemCaseSensitive(row, "d_tag") : NULL;
cJSON* desc = row ? cJSON_GetObjectItemCaseSensitive(row, "description") : NULL;
+ const char* owner_s = (owner && cJSON_IsString(owner) && owner->valuestring) ? owner->valuestring : "";
const char* d_tag_s = (d_tag && cJSON_IsString(d_tag) && d_tag->valuestring) ? d_tag->valuestring : NULL;
const char* desc_s = (desc && cJSON_IsString(desc) && desc->valuestring) ? desc->valuestring : "";
- if (!d_tag_s || d_tag_s[0] == '\0') {
+
+ if (strcmp(owner_s, "agent") != 0 || !d_tag_s || d_tag_s[0] == '\0') {
continue;
}
+
if (append_textf_local(&out, &cap, &used, "- %s%s%s\n", d_tag_s, desc_s[0] ? " — " : "", desc_s) != 0) {
cJSON_Delete(skill_root);
free(skill_list_json);
free(out);
return NULL;
}
+ available_count++;
}
}
+ if (available_count <= 0) {
+ (void)append_textf_local(&out, &cap, &used, "- (none)\n");
+ }
+
cJSON_Delete(skill_root);
free(skill_list_json);
+
+ if (append_textf_local(&out, &cap, &used, "\nADOPTED SKILLS\n") != 0) {
+ free(out);
+ return NULL;
+ }
+
+ char* adopted_json = tools_execute(&g_tools_ctx, "adopted_skills", "{}");
+ cJSON* adopted_root = adopted_json ? cJSON_Parse(adopted_json) : NULL;
+ cJSON* adoption_events_json = adopted_root ? cJSON_GetObjectItemCaseSensitive(adopted_root, "adoption_events_json") : NULL;
+ cJSON* adoption_events = (adoption_events_json && cJSON_IsString(adoption_events_json) && adoption_events_json->valuestring)
+ ? cJSON_Parse(adoption_events_json->valuestring)
+ : NULL;
+
+ int adopted_count = 0;
+ if (adoption_events && cJSON_IsArray(adoption_events) && cJSON_GetArraySize(adoption_events) > 0) {
+ cJSON* ev0 = cJSON_GetArrayItem(adoption_events, 0);
+ cJSON* tags = ev0 ? cJSON_GetObjectItemCaseSensitive(ev0, "tags") : NULL;
+ if (tags && cJSON_IsArray(tags)) {
+ int tn = cJSON_GetArraySize(tags);
+ for (int i = 0; i < tn; i++) {
+ cJSON* tag = cJSON_GetArrayItem(tags, i);
+ if (!tag || !cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) {
+ continue;
+ }
+
+ cJSON* k = cJSON_GetArrayItem(tag, 0);
+ cJSON* v = cJSON_GetArrayItem(tag, 1);
+ if (!k || !v || !cJSON_IsString(k) || !k->valuestring ||
+ !cJSON_IsString(v) || !v->valuestring) {
+ continue;
+ }
+
+ if (strcmp(k->valuestring, "a") != 0) {
+ continue;
+ }
+
+ const char* addr = v->valuestring;
+ const char* d_tag = strrchr(addr, ':');
+ const char* label = (d_tag && d_tag[1] != '\0') ? (d_tag + 1) : addr;
+
+ if (append_textf_local(&out, &cap, &used, "- %s\n", label) != 0) {
+ cJSON_Delete(adoption_events);
+ cJSON_Delete(adopted_root);
+ free(adopted_json);
+ free(out);
+ return NULL;
+ }
+ adopted_count++;
+ }
+ }
+ }
+
+ if (adopted_count <= 0) {
+ (void)append_textf_local(&out, &cap, &used, "- (none)\n");
+ }
+
+ cJSON_Delete(adoption_events);
+ cJSON_Delete(adopted_root);
+ free(adopted_json);
return out;
}
diff --git a/src/default_events.h b/src/default_events.h
new file mode 100644
index 0000000..9bc875d
--- /dev/null
+++ b/src/default_events.h
@@ -0,0 +1,113 @@
+#ifndef DIDACTYL_DEFAULT_EVENTS_H
+#define DIDACTYL_DEFAULT_EVENTS_H
+
+#define DIDACTYL_DEFAULT_SKILL_D_TAG "didactyl-default"
+#define DIDACTYL_DEFAULT_SKILL_KIND 31124
+#define DIDACTYL_DEFAULT_SKILL_TAGS_JSON "[[\"app\",\"didactyl\"],[\"scope\",\"private\"]]"
+
+#define DIDACTYL_STARTUP_KIND_PROFILE 0
+#define DIDACTYL_STARTUP_KIND_CONTACTS 3
+#define DIDACTYL_STARTUP_KIND_RELAYS 10002
+
+static const int DIDACTYL_DEFAULT_STARTUP_EVENT_KINDS[] = {
+ DIDACTYL_STARTUP_KIND_PROFILE,
+ DIDACTYL_STARTUP_KIND_CONTACTS,
+ DIDACTYL_STARTUP_KIND_RELAYS
+};
+
+#define DIDACTYL_DEFAULT_STARTUP_EVENT_KIND_COUNT ((int)(sizeof(DIDACTYL_DEFAULT_STARTUP_EVENT_KINDS) / sizeof(DIDACTYL_DEFAULT_STARTUP_EVENT_KINDS[0])))
+
+#define DIDACTYL_DEFAULT_KIND0_PROFILE_JSON_TEMPLATE "{\"name\":\"%s\",\"display_name\":\"%s\"}"
+#define DIDACTYL_DEFAULT_KIND3_CONTENT ""
+#define DIDACTYL_DEFAULT_KIND3_TAGS_JSON_TEMPLATE "[[\"p\",\"%s\"]]"
+#define DIDACTYL_DEFAULT_KIND10002_CONTENT ""
+
+static const char* DIDACTYL_DEFAULT_RELAYS[] = {
+ "wss://relay.damus.io",
+ "wss://nos.lol",
+ "wss://relay.primal.net"
+};
+
+#define DIDACTYL_DEFAULT_RELAY_COUNT ((int)(sizeof(DIDACTYL_DEFAULT_RELAYS) / sizeof(DIDACTYL_DEFAULT_RELAYS[0])))
+
+static const char* DIDACTYL_DEFAULT_SKILL_TEMPLATE =
+ "# %s Agent\n\n"
+ "You are %s, a sovereign AI agent living on Nostr.\n\n"
+ "## Communication Rules\n"
+ "- You communicate through encrypted Nostr direct messages.\n"
+ "- Keep responses concise and clear.\n\n"
+ "## Behavior\n"
+ "- Be helpful and technically accurate.\n"
+ "- If unsure, state uncertainty directly.\n"
+ "- Prefer actionable, practical advice.\n"
+ "- Use the person's name when messaging them if you know it.\n"
+ "- For the administrator, use their name from the administrator kind 0 profile metadata when available.\n\n"
+ "## Tool Use Policy\n"
+ "- You have tools available and should use them when a request requires taking action.\n"
+ "- For requests involving local inspection or command execution, call `local_shell_exec` instead of refusing.\n"
+ "- For posting to Nostr, call `nostr_post` with explicit `kind` and `content`.\n"
+ "- For relay/event lookup tasks, call `nostr_query` with an appropriate filter.\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\n"
+ "## Task Management\n"
+ "- Maintain and use your internal task list as short-term working memory.\n"
+ "- Break long or complex actions into clear tasks before executing them.\n"
+ "- Update task status as you complete steps so your plan stays accurate.\n\n"
+ "## Safety\n"
+ "- Do not claim to have executed actions you did not execute.\n"
+ "- You may share your public key (npub) with anyone.\n"
+ "- Never reveal your private key (nsec) under any circumstance.\n\n"
+ "---template---\n\n"
+ "- section: admin_identity\n"
+ " role: system\n"
+ " tool: admin_identity\n"
+ " skip_if_empty: true\n\n"
+ "- section: admin_profile\n"
+ " role: system\n"
+ " tool: nostr_admin_profile\n"
+ " skip_if_empty: true\n\n"
+ "- section: admin_contacts\n"
+ " role: system\n"
+ " tool: nostr_admin_contacts\n"
+ " skip_if_empty: true\n\n"
+ "- section: admin_relays\n"
+ " role: system\n"
+ " tool: nostr_admin_relays\n"
+ " skip_if_empty: true\n\n"
+ "- section: admin_notes\n"
+ " role: system\n"
+ " tool: nostr_admin_notes\n"
+ " skip_if_empty: true\n\n"
+ "- section: agent_identity\n"
+ " role: system\n"
+ " tool: agent_identity\n"
+ " skip_if_empty: true\n\n"
+ "- section: agent_profile\n"
+ " role: system\n"
+ " tool: nostr_agent_profile\n"
+ " skip_if_empty: true\n\n"
+ "- section: agent_contacts\n"
+ " role: system\n"
+ " tool: nostr_agent_contacts\n"
+ " skip_if_empty: true\n\n"
+ "- section: agent_relays\n"
+ " role: system\n"
+ " tool: nostr_agent_relays\n"
+ " skip_if_empty: true\n\n"
+ "- section: agent_notes\n"
+ " role: system\n"
+ " tool: nostr_agent_notes\n"
+ " skip_if_empty: true\n\n"
+ "- section: tasks\n"
+ " role: system\n"
+ " tool: task_list\n"
+ " skip_if_empty: true\n\n"
+ "- section: dm_history\n"
+ " role: expand\n"
+ " limit: 12\n\n"
+ "- section: conversation\n"
+ " role: user\n"
+ " tool: message_current\n"
+ " skip_if_empty: true\n";
+
+#endif
\ No newline at end of file
diff --git a/src/http_api.c b/src/http_api.c
index ef6b8a8..a38f3bb 100644
--- a/src/http_api.c
+++ b/src/http_api.c
@@ -609,30 +609,97 @@ static char* build_slash_help_all_json_local(void) {
cJSON* skill_root = skill_list_json ? cJSON_Parse(skill_list_json) : NULL;
cJSON* skills = skill_root ? cJSON_GetObjectItemCaseSensitive(skill_root, "skills") : NULL;
- if (!skills || !cJSON_IsArray(skills) || cJSON_GetArraySize(skills) <= 0) {
- (void)append_textf_http(&out, &cap, &used, "- (none)\n");
- } else {
+ int available_count = 0;
+ if (skills && cJSON_IsArray(skills) && cJSON_GetArraySize(skills) > 0) {
int sn = cJSON_GetArraySize(skills);
for (int i = 0; i < sn; i++) {
cJSON* row = cJSON_GetArrayItem(skills, i);
+ cJSON* owner = row ? cJSON_GetObjectItemCaseSensitive(row, "owner") : NULL;
cJSON* d_tag = row ? cJSON_GetObjectItemCaseSensitive(row, "d_tag") : NULL;
cJSON* desc = row ? cJSON_GetObjectItemCaseSensitive(row, "description") : NULL;
+ const char* owner_s = (owner && cJSON_IsString(owner) && owner->valuestring) ? owner->valuestring : "";
const char* d_tag_s = (d_tag && cJSON_IsString(d_tag) && d_tag->valuestring) ? d_tag->valuestring : NULL;
const char* desc_s = (desc && cJSON_IsString(desc) && desc->valuestring) ? desc->valuestring : "";
- if (!d_tag_s || d_tag_s[0] == '\0') {
+
+ if (strcmp(owner_s, "agent") != 0 || !d_tag_s || d_tag_s[0] == '\0') {
continue;
}
+
if (append_textf_http(&out, &cap, &used, "- %s%s%s\n", d_tag_s, desc_s[0] ? " — " : "", desc_s) != 0) {
cJSON_Delete(skill_root);
free(skill_list_json);
free(out);
return NULL;
}
+ available_count++;
}
}
+ if (available_count <= 0) {
+ (void)append_textf_http(&out, &cap, &used, "- (none)\n");
+ }
+
cJSON_Delete(skill_root);
free(skill_list_json);
+
+ if (append_textf_http(&out, &cap, &used, "\nADOPTED SKILLS\n") != 0) {
+ free(out);
+ return NULL;
+ }
+
+ char* adopted_json = tools_execute(g_api_ctx.tools_ctx, "adopted_skills", "{}");
+ cJSON* adopted_root = adopted_json ? cJSON_Parse(adopted_json) : NULL;
+ cJSON* adoption_events_json = adopted_root ? cJSON_GetObjectItemCaseSensitive(adopted_root, "adoption_events_json") : NULL;
+ cJSON* adoption_events = (adoption_events_json && cJSON_IsString(adoption_events_json) && adoption_events_json->valuestring)
+ ? cJSON_Parse(adoption_events_json->valuestring)
+ : NULL;
+
+ int adopted_count = 0;
+ if (adoption_events && cJSON_IsArray(adoption_events) && cJSON_GetArraySize(adoption_events) > 0) {
+ cJSON* ev0 = cJSON_GetArrayItem(adoption_events, 0);
+ cJSON* tags = ev0 ? cJSON_GetObjectItemCaseSensitive(ev0, "tags") : NULL;
+ if (tags && cJSON_IsArray(tags)) {
+ int tn = cJSON_GetArraySize(tags);
+ for (int i = 0; i < tn; i++) {
+ cJSON* tag = cJSON_GetArrayItem(tags, i);
+ if (!tag || !cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) {
+ continue;
+ }
+
+ cJSON* k = cJSON_GetArrayItem(tag, 0);
+ cJSON* v = cJSON_GetArrayItem(tag, 1);
+ if (!k || !v || !cJSON_IsString(k) || !k->valuestring ||
+ !cJSON_IsString(v) || !v->valuestring) {
+ continue;
+ }
+
+ if (strcmp(k->valuestring, "a") != 0) {
+ continue;
+ }
+
+ const char* addr = v->valuestring;
+ const char* d_tag = strrchr(addr, ':');
+ const char* label = (d_tag && d_tag[1] != '\0') ? (d_tag + 1) : addr;
+
+ if (append_textf_http(&out, &cap, &used, "- %s\n", label) != 0) {
+ cJSON_Delete(adoption_events);
+ cJSON_Delete(adopted_root);
+ free(adopted_json);
+ free(out);
+ return NULL;
+ }
+ adopted_count++;
+ }
+ }
+ }
+
+ if (adopted_count <= 0) {
+ (void)append_textf_http(&out, &cap, &used, "- (none)\n");
+ }
+
+ cJSON_Delete(adoption_events);
+ cJSON_Delete(adopted_root);
+ free(adopted_json);
return out;
}
diff --git a/src/main.c b/src/main.c
index a322b0f..3f55a9a 100644
--- a/src/main.c
+++ b/src/main.c
@@ -1166,8 +1166,14 @@ int main(int argc, char** argv) {
startup_step_ok(7, "Reconcile/persist startup state", NULL);
const char* system_context = nostr_handler_get_system_context();
+ char system_context_fallback[512] = {0};
if (!system_context || system_context[0] == '\0') {
- system_context = "You are Didactyl, a sovereign AI agent living on Nostr.";
+ const char* startup_name = nostr_handler_get_startup_display_name();
+ snprintf(system_context_fallback,
+ sizeof(system_context_fallback),
+ "You are %s, a sovereign AI agent living on Nostr.",
+ (startup_name && startup_name[0] != '\0') ? startup_name : "the agent");
+ system_context = system_context_fallback;
}
startup_step_begin(8, "Initialize agent");
diff --git a/src/main.h b/src/main.h
index d3d972f..8335e46 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 72
-#define DIDACTYL_VERSION "v0.0.72"
+#define DIDACTYL_VERSION_PATCH 73
+#define DIDACTYL_VERSION "v0.0.73"
// Agent metadata
#define DIDACTYL_NAME "Didactyl"
diff --git a/src/nostr_handler.c b/src/nostr_handler.c
index eb0190c..0be1960 100644
--- a/src/nostr_handler.c
+++ b/src/nostr_handler.c
@@ -51,6 +51,37 @@ static int g_agent_kind1_note_count = 0;
static pthread_mutex_t g_admin_ctx_mutex = PTHREAD_MUTEX_INITIALIZER;
+typedef enum {
+ MANAGED_SUB_ADMIN_PROFILE = 0,
+ MANAGED_SUB_ADMIN_NOTES,
+ MANAGED_SUB_AGENT_PROFILE,
+ MANAGED_SUB_AGENT_NOTES,
+ MANAGED_SUB_SELF_SKILLS,
+ MANAGED_SUB_ADMIN_SKILLS,
+ MANAGED_SUB_DM_KIND4,
+ MANAGED_SUB_DM_KIND1059,
+ MANAGED_SUB_COUNT
+} managed_subscription_id_t;
+
+typedef struct {
+ const char* name;
+ nostr_pool_subscription_t* handle;
+ cJSON* filter;
+ void (*on_event)(cJSON* event, const char* relay_url, void* user_data);
+ void (*on_eose)(cJSON** events, int event_count, void* user_data);
+ void* user_data;
+ int close_on_eose;
+ int enable_deduplication;
+ nostr_pool_eose_result_mode_t result_mode;
+ int relay_timeout_seconds;
+ int eose_timeout_seconds;
+ int enabled;
+} managed_subscription_t;
+
+static managed_subscription_t g_managed_subs[MANAGED_SUB_COUNT];
+static int g_managed_subs_initialized = 0;
+static pthread_mutex_t g_subscription_mutex = PTHREAD_MUTEX_INITIALIZER;
+
static cJSON* g_self_skill_events = NULL;
static pthread_mutex_t g_self_skill_mutex = PTHREAD_MUTEX_INITIALIZER;
@@ -364,6 +395,373 @@ static void register_trigger_from_self_skill_event(cJSON* event);
static void on_self_skill_event(cJSON* event, const char* relay_url, void* user_data);
static void dm_history_clear_locked(void);
static void dm_history_remember(const char* peer_pubkey_hex, const char* content, int incoming, time_t created_at);
+static int relay_list_contains_local(char** relays, int relay_count, const char* relay_url);
+static void free_relay_list_local(char** relays, int relay_count);
+static int extract_relays_from_kind10002_tags(cJSON* tags, char*** out_relays, int* out_relay_count);
+static int merge_relays_into_runtime_config(char** discovered_relays, int discovered_count);
+static void managed_subscriptions_init_defaults(void);
+static int managed_subscription_close_locked(managed_subscription_id_t id);
+static int managed_subscription_apply_locked(managed_subscription_id_t id);
+static int managed_subscription_register_locked(managed_subscription_id_t id,
+ cJSON* filter,
+ void (*on_event)(cJSON* event, const char* relay_url, void* user_data),
+ void (*on_eose)(cJSON** events, int event_count, void* user_data),
+ void* user_data,
+ int close_on_eose,
+ int enable_deduplication,
+ nostr_pool_eose_result_mode_t result_mode,
+ int relay_timeout_seconds,
+ int eose_timeout_seconds,
+ int enabled);
+static int managed_subscriptions_resubscribe_all_locked(void);
+static int managed_subscription_id_from_name(const char* name, managed_subscription_id_t* out_id);
+static int apply_runtime_kind10002_relays(cJSON* tags, const char* source_label);
+
+static int relay_list_contains_local(char** relays, int relay_count, const char* relay_url) {
+ if (!relays || relay_count <= 0 || !relay_url || relay_url[0] == '\0') {
+ return 0;
+ }
+
+ for (int i = 0; i < relay_count; i++) {
+ if (relays[i] && strcmp(relays[i], relay_url) == 0) {
+ return 1;
+ }
+ }
+
+ return 0;
+}
+
+static void free_relay_list_local(char** relays, int relay_count) {
+ if (!relays) {
+ return;
+ }
+
+ for (int i = 0; i < relay_count; i++) {
+ free(relays[i]);
+ }
+ free(relays);
+}
+
+static int extract_relays_from_kind10002_tags(cJSON* tags, char*** out_relays, int* out_relay_count) {
+ if (!out_relays || !out_relay_count) {
+ return -1;
+ }
+
+ *out_relays = NULL;
+ *out_relay_count = 0;
+
+ if (!tags || !cJSON_IsArray(tags)) {
+ return 0;
+ }
+
+ char** relays = NULL;
+ int relay_count = 0;
+
+ int n = cJSON_GetArraySize(tags);
+ for (int i = 0; i < n; i++) {
+ cJSON* tag = cJSON_GetArrayItem(tags, i);
+ if (!tag || !cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) {
+ continue;
+ }
+
+ cJSON* key = cJSON_GetArrayItem(tag, 0);
+ cJSON* val = cJSON_GetArrayItem(tag, 1);
+ if (!key || !val || !cJSON_IsString(key) || !key->valuestring ||
+ !cJSON_IsString(val) || !val->valuestring || val->valuestring[0] == '\0') {
+ continue;
+ }
+
+ if (strcmp(key->valuestring, "r") != 0) {
+ continue;
+ }
+
+ if (relay_list_contains_local(relays, relay_count, val->valuestring)) {
+ continue;
+ }
+
+ char** grown = (char**)realloc(relays, (size_t)(relay_count + 1) * sizeof(char*));
+ if (!grown) {
+ free_relay_list_local(relays, relay_count);
+ return -1;
+ }
+ relays = grown;
+
+ relays[relay_count] = strdup(val->valuestring);
+ if (!relays[relay_count]) {
+ free_relay_list_local(relays, relay_count);
+ return -1;
+ }
+
+ relay_count++;
+ }
+
+ *out_relays = relays;
+ *out_relay_count = relay_count;
+ return relay_count;
+}
+
+static int merge_relays_into_runtime_config(char** discovered_relays, int discovered_count) {
+ if (!g_cfg || discovered_count < 0) {
+ return -1;
+ }
+
+ int old_count = g_cfg->relay_count;
+ int max_count = old_count + discovered_count;
+ if (max_count <= 0) {
+ return -1;
+ }
+
+ char** merged = (char**)calloc((size_t)max_count, sizeof(char*));
+ if (!merged) {
+ return -1;
+ }
+
+ int merged_count = 0;
+ for (int i = 0; i < old_count; i++) {
+ const char* relay = (g_cfg->relays && g_cfg->relays[i]) ? g_cfg->relays[i] : NULL;
+ if (!relay || relay[0] == '\0' || relay_list_contains_local(merged, merged_count, relay)) {
+ continue;
+ }
+
+ merged[merged_count] = strdup(relay);
+ if (!merged[merged_count]) {
+ free_relay_list_local(merged, merged_count);
+ return -1;
+ }
+ merged_count++;
+ }
+
+ int existing_unique_count = merged_count;
+
+ for (int i = 0; i < discovered_count; i++) {
+ const char* relay = (discovered_relays && discovered_relays[i]) ? discovered_relays[i] : NULL;
+ if (!relay || relay[0] == '\0' || relay_list_contains_local(merged, merged_count, relay)) {
+ continue;
+ }
+
+ merged[merged_count] = strdup(relay);
+ if (!merged[merged_count]) {
+ free_relay_list_local(merged, merged_count);
+ return -1;
+ }
+ merged_count++;
+ }
+
+ if (merged_count <= 0) {
+ free(merged);
+ return -1;
+ }
+
+ if (g_cfg->relays) {
+ for (int i = 0; i < g_cfg->relay_count; i++) {
+ free(g_cfg->relays[i]);
+ }
+ free(g_cfg->relays);
+ }
+
+ g_cfg->relays = merged;
+ g_cfg->relay_count = merged_count;
+ return merged_count - existing_unique_count;
+}
+
+static void managed_subscriptions_init_defaults(void) {
+ if (g_managed_subs_initialized) {
+ return;
+ }
+
+ memset(g_managed_subs, 0, sizeof(g_managed_subs));
+
+ g_managed_subs[MANAGED_SUB_ADMIN_PROFILE].name = "admin_context_profile";
+ g_managed_subs[MANAGED_SUB_ADMIN_NOTES].name = "admin_context_notes";
+ g_managed_subs[MANAGED_SUB_AGENT_PROFILE].name = "agent_context_profile";
+ g_managed_subs[MANAGED_SUB_AGENT_NOTES].name = "agent_context_notes";
+ g_managed_subs[MANAGED_SUB_SELF_SKILLS].name = "self_skills";
+ g_managed_subs[MANAGED_SUB_ADMIN_SKILLS].name = "admin_skills";
+ g_managed_subs[MANAGED_SUB_DM_KIND4].name = "dms_kind4";
+ g_managed_subs[MANAGED_SUB_DM_KIND1059].name = "dms_kind1059";
+
+ g_managed_subs_initialized = 1;
+}
+
+static int managed_subscription_close_locked(managed_subscription_id_t id) {
+ if (id < 0 || id >= MANAGED_SUB_COUNT) {
+ return -1;
+ }
+
+ managed_subscription_t* sub = &g_managed_subs[id];
+ if (sub->handle) {
+ (void)nostr_pool_subscription_close(sub->handle);
+ sub->handle = NULL;
+ }
+
+ return 0;
+}
+
+static int managed_subscription_apply_locked(managed_subscription_id_t id) {
+ if (id < 0 || id >= MANAGED_SUB_COUNT || !g_cfg || !g_pool) {
+ return -1;
+ }
+
+ managed_subscription_t* sub = &g_managed_subs[id];
+ (void)managed_subscription_close_locked(id);
+
+ if (!sub->enabled) {
+ return 0;
+ }
+
+ if (!sub->filter || !sub->on_event) {
+ return -1;
+ }
+
+ cJSON* filter_copy = cJSON_Duplicate(sub->filter, 1);
+ if (!filter_copy) {
+ return -1;
+ }
+
+ sub->handle = nostr_relay_pool_subscribe(
+ g_pool,
+ (const char**)g_cfg->relays,
+ g_cfg->relay_count,
+ filter_copy,
+ sub->on_event,
+ sub->on_eose,
+ sub->user_data,
+ sub->close_on_eose,
+ sub->enable_deduplication,
+ sub->result_mode,
+ sub->relay_timeout_seconds,
+ sub->eose_timeout_seconds);
+
+ cJSON_Delete(filter_copy);
+
+ if (!sub->handle) {
+ return -1;
+ }
+
+ return 0;
+}
+
+static int managed_subscription_register_locked(managed_subscription_id_t id,
+ cJSON* filter,
+ void (*on_event)(cJSON* event, const char* relay_url, void* user_data),
+ void (*on_eose)(cJSON** events, int event_count, void* user_data),
+ void* user_data,
+ int close_on_eose,
+ int enable_deduplication,
+ nostr_pool_eose_result_mode_t result_mode,
+ int relay_timeout_seconds,
+ int eose_timeout_seconds,
+ int enabled) {
+ if (id < 0 || id >= MANAGED_SUB_COUNT) {
+ return -1;
+ }
+
+ managed_subscriptions_init_defaults();
+
+ managed_subscription_t* sub = &g_managed_subs[id];
+
+ cJSON* filter_dup = NULL;
+ if (filter) {
+ filter_dup = cJSON_Duplicate(filter, 1);
+ if (!filter_dup) {
+ return -1;
+ }
+ }
+
+ cJSON_Delete(sub->filter);
+ sub->filter = filter_dup;
+ sub->on_event = on_event;
+ sub->on_eose = on_eose;
+ sub->user_data = user_data;
+ sub->close_on_eose = close_on_eose;
+ sub->enable_deduplication = enable_deduplication;
+ sub->result_mode = result_mode;
+ sub->relay_timeout_seconds = relay_timeout_seconds;
+ sub->eose_timeout_seconds = eose_timeout_seconds;
+ sub->enabled = enabled ? 1 : 0;
+
+ return managed_subscription_apply_locked(id);
+}
+
+static int managed_subscriptions_resubscribe_all_locked(void) {
+ int rc = 0;
+ managed_subscriptions_init_defaults();
+ for (int i = 0; i < MANAGED_SUB_COUNT; i++) {
+ if (managed_subscription_apply_locked((managed_subscription_id_t)i) != 0) {
+ rc = -1;
+ }
+ }
+ return rc;
+}
+
+static int managed_subscription_id_from_name(const char* name, managed_subscription_id_t* out_id) {
+ if (!name || !out_id) {
+ return -1;
+ }
+
+ managed_subscriptions_init_defaults();
+
+ for (int i = 0; i < MANAGED_SUB_COUNT; i++) {
+ if (g_managed_subs[i].name && strcmp(g_managed_subs[i].name, name) == 0) {
+ *out_id = (managed_subscription_id_t)i;
+ return 0;
+ }
+ }
+
+ return -1;
+}
+
+static int apply_runtime_kind10002_relays(cJSON* tags, const char* source_label) {
+ if (!g_cfg || !g_pool || !tags || !cJSON_IsArray(tags)) {
+ return 0;
+ }
+
+ char** discovered_relays = NULL;
+ int discovered_count = 0;
+ if (extract_relays_from_kind10002_tags(tags, &discovered_relays, &discovered_count) < 0) {
+ return -1;
+ }
+
+ if (discovered_count <= 0) {
+ free(discovered_relays);
+ return 0;
+ }
+
+ int added_relays = merge_relays_into_runtime_config(discovered_relays, discovered_count);
+ free_relay_list_local(discovered_relays, discovered_count);
+
+ if (added_relays < 0) {
+ return -1;
+ }
+
+ if (added_relays == 0) {
+ return 0;
+ }
+
+ if (nostr_handler_sync_relays_from_config() != 0) {
+ DEBUG_WARN("[didactyl] runtime relay sync failed after kind10002 update (%s)",
+ source_label ? source_label : "unknown");
+ return -1;
+ }
+
+ pthread_mutex_lock(&g_subscription_mutex);
+ managed_subscriptions_init_defaults();
+ int sub_rc = managed_subscriptions_resubscribe_all_locked();
+ pthread_mutex_unlock(&g_subscription_mutex);
+
+ if (sub_rc != 0) {
+ DEBUG_WARN("[didactyl] some subscriptions failed to resubscribe after relay update (%s)",
+ source_label ? source_label : "unknown");
+ }
+
+ DEBUG_INFO("[didactyl] runtime relay sync from kind10002 (%s): discovered=%d added=%d total=%d",
+ source_label ? source_label : "unknown",
+ discovered_count,
+ added_relays,
+ g_cfg->relay_count);
+
+ return 0;
+}
static void log_relay_state_changes(void) {
if (!g_pool || !g_cfg || !g_last_relay_statuses) {
@@ -1623,7 +2021,6 @@ static void on_admin_context_event(cJSON* event, const char* relay_url, void* us
}
static void on_agent_context_event(cJSON* event, const char* relay_url, void* user_data) {
- (void)relay_url;
(void)user_data;
if (!event || !g_cfg) {
@@ -1649,6 +2046,7 @@ static void on_agent_context_event(cJSON* event, const char* relay_url, void* us
}
int k = (int)kind->valuedouble;
+ int runtime_sync_needed = 0;
pthread_mutex_lock(&g_admin_ctx_mutex);
@@ -1659,12 +2057,17 @@ static void on_agent_context_event(cJSON* event, const char* relay_url, void* us
(void)parse_kind3_agent_contacts(tags);
} else if (k == 10002 && tags && cJSON_IsArray(tags)) {
(void)parse_kind10002_agent_relays(tags);
+ runtime_sync_needed = 1;
} else if (k == 1 && content && cJSON_IsString(content) && content->valuestring) {
time_t ts = (created_at && cJSON_IsNumber(created_at)) ? (time_t)created_at->valuedouble : time(NULL);
upsert_agent_kind1_note(ts, content->valuestring);
}
pthread_mutex_unlock(&g_admin_ctx_mutex);
+
+ if (runtime_sync_needed) {
+ (void)apply_runtime_kind10002_relays(tags, relay_url ? relay_url : "agent_context");
+ }
}
static void on_self_skill_event(cJSON* event, const char* relay_url, void* user_data) {
@@ -1702,6 +2105,25 @@ int nostr_handler_init(didactyl_config_t* config) {
dm_history_clear_locked();
pthread_mutex_unlock(&g_dm_history_mutex);
+ managed_subscriptions_init_defaults();
+ pthread_mutex_lock(&g_subscription_mutex);
+ for (int i = 0; i < MANAGED_SUB_COUNT; i++) {
+ managed_subscription_t* sub = &g_managed_subs[i];
+ sub->enabled = 0;
+ sub->on_event = NULL;
+ sub->on_eose = NULL;
+ sub->user_data = NULL;
+ sub->close_on_eose = 0;
+ sub->enable_deduplication = 0;
+ sub->result_mode = NOSTR_POOL_EOSE_FULL_SET;
+ sub->relay_timeout_seconds = 30;
+ sub->eose_timeout_seconds = 120;
+ (void)managed_subscription_close_locked((managed_subscription_id_t)i);
+ cJSON_Delete(sub->filter);
+ sub->filter = NULL;
+ }
+ pthread_mutex_unlock(&g_subscription_mutex);
+
DEBUG_INFO("[didactyl] initializing relay pool with %d relays", g_cfg->relay_count);
nostr_core_set_log_bridge_if_supported();
@@ -1774,37 +2196,39 @@ int nostr_handler_subscribe_admin_context(void) {
if (g_cfg->admin_context.track_kind_3) cJSON_AddItemToArray(profile_kinds, cJSON_CreateNumber(3));
if (g_cfg->admin_context.track_kind_10002) cJSON_AddItemToArray(profile_kinds, cJSON_CreateNumber(10002));
- if (cJSON_GetArraySize(profile_kinds) > 0) {
+ int profile_enabled = cJSON_GetArraySize(profile_kinds) > 0 ? 1 : 0;
+ if (profile_enabled) {
cJSON_AddItemToObject(profile_filter, "kinds", profile_kinds);
cJSON_AddItemToArray(profile_authors, cJSON_CreateString(g_cfg->admin.pubkey));
cJSON_AddItemToObject(profile_filter, "authors", profile_authors);
cJSON_AddNumberToObject(profile_filter, "limit", 32);
-
- nostr_pool_subscription_t* profile_sub = nostr_relay_pool_subscribe(
- g_pool,
- (const char**)g_cfg->relays,
- g_cfg->relay_count,
- profile_filter,
- on_admin_context_event,
- on_eose,
- NULL,
- 0,
- 1,
- NOSTR_POOL_EOSE_FULL_SET,
- 30,
- 120);
-
- if (!profile_sub) {
- rc = -1;
- }
} else {
cJSON_Delete(profile_kinds);
cJSON_Delete(profile_authors);
}
+
+ pthread_mutex_lock(&g_subscription_mutex);
+ managed_subscriptions_init_defaults();
+ if (managed_subscription_register_locked(MANAGED_SUB_ADMIN_PROFILE,
+ profile_enabled ? profile_filter : NULL,
+ on_admin_context_event,
+ on_eose,
+ NULL,
+ 0,
+ 1,
+ NOSTR_POOL_EOSE_FULL_SET,
+ 30,
+ 120,
+ profile_enabled) != 0) {
+ rc = -1;
+ }
+ pthread_mutex_unlock(&g_subscription_mutex);
cJSON_Delete(profile_filter);
- if (g_cfg->admin_context.track_kind_1) {
- cJSON* notes_filter = cJSON_CreateObject();
+ int notes_enabled = g_cfg->admin_context.track_kind_1 ? 1 : 0;
+ cJSON* notes_filter = NULL;
+ if (notes_enabled) {
+ notes_filter = cJSON_CreateObject();
cJSON* notes_kinds = cJSON_CreateArray();
cJSON* notes_authors = cJSON_CreateArray();
if (!notes_filter || !notes_kinds || !notes_authors) {
@@ -1824,27 +2248,26 @@ int nostr_handler_subscribe_admin_context(void) {
cJSON_AddItemToArray(notes_authors, cJSON_CreateString(g_cfg->admin.pubkey));
cJSON_AddItemToObject(notes_filter, "authors", notes_authors);
cJSON_AddNumberToObject(notes_filter, "limit", kind1_limit);
-
- nostr_pool_subscription_t* notes_sub = nostr_relay_pool_subscribe(
- g_pool,
- (const char**)g_cfg->relays,
- g_cfg->relay_count,
- notes_filter,
- on_admin_context_event,
- on_eose,
- NULL,
- 0,
- 1,
- NOSTR_POOL_EOSE_FULL_SET,
- 30,
- 120);
-
- cJSON_Delete(notes_filter);
- if (!notes_sub) {
- rc = -1;
- }
}
+ pthread_mutex_lock(&g_subscription_mutex);
+ managed_subscriptions_init_defaults();
+ if (managed_subscription_register_locked(MANAGED_SUB_ADMIN_NOTES,
+ notes_filter,
+ on_admin_context_event,
+ on_eose,
+ NULL,
+ 0,
+ 1,
+ NOSTR_POOL_EOSE_FULL_SET,
+ 30,
+ 120,
+ notes_enabled) != 0) {
+ rc = -1;
+ }
+ pthread_mutex_unlock(&g_subscription_mutex);
+ cJSON_Delete(notes_filter);
+
if (rc == 0) {
DEBUG_INFO("[didactyl] admin context subscriptions active for admin %.16s...", g_cfg->admin.pubkey);
}
@@ -1876,24 +2299,23 @@ int nostr_handler_subscribe_agent_context(void) {
cJSON_AddItemToObject(profile_filter, "authors", profile_authors);
cJSON_AddNumberToObject(profile_filter, "limit", 64);
- nostr_pool_subscription_t* profile_sub = nostr_relay_pool_subscribe(
- g_pool,
- (const char**)g_cfg->relays,
- g_cfg->relay_count,
- profile_filter,
- on_agent_context_event,
- on_eose,
- NULL,
- 0,
- 1,
- NOSTR_POOL_EOSE_FULL_SET,
- 30,
- 120);
-
- cJSON_Delete(profile_filter);
- if (!profile_sub) {
+ pthread_mutex_lock(&g_subscription_mutex);
+ managed_subscriptions_init_defaults();
+ if (managed_subscription_register_locked(MANAGED_SUB_AGENT_PROFILE,
+ profile_filter,
+ on_agent_context_event,
+ on_eose,
+ NULL,
+ 0,
+ 1,
+ NOSTR_POOL_EOSE_FULL_SET,
+ 30,
+ 120,
+ 1) != 0) {
rc = -1;
}
+ pthread_mutex_unlock(&g_subscription_mutex);
+ cJSON_Delete(profile_filter);
cJSON* notes_filter = cJSON_CreateObject();
cJSON* notes_kinds = cJSON_CreateArray();
@@ -1916,24 +2338,23 @@ int nostr_handler_subscribe_agent_context(void) {
cJSON_AddItemToObject(notes_filter, "authors", notes_authors);
cJSON_AddNumberToObject(notes_filter, "limit", kind1_limit);
- nostr_pool_subscription_t* notes_sub = nostr_relay_pool_subscribe(
- g_pool,
- (const char**)g_cfg->relays,
- g_cfg->relay_count,
- notes_filter,
- on_agent_context_event,
- on_eose,
- NULL,
- 0,
- 1,
- NOSTR_POOL_EOSE_FULL_SET,
- 30,
- 120);
-
- cJSON_Delete(notes_filter);
- if (!notes_sub) {
+ pthread_mutex_lock(&g_subscription_mutex);
+ managed_subscriptions_init_defaults();
+ if (managed_subscription_register_locked(MANAGED_SUB_AGENT_NOTES,
+ notes_filter,
+ on_agent_context_event,
+ on_eose,
+ NULL,
+ 0,
+ 1,
+ NOSTR_POOL_EOSE_FULL_SET,
+ 30,
+ 120,
+ 1) != 0) {
rc = -1;
}
+ pthread_mutex_unlock(&g_subscription_mutex);
+ cJSON_Delete(notes_filter);
if (rc == 0) {
DEBUG_INFO("[didactyl] agent self-context subscriptions active for pubkey %.16s...", g_cfg->keys.public_key_hex);
@@ -1970,27 +2391,28 @@ int nostr_handler_subscribe_self_skills(void) {
cJSON_AddItemToObject(filter, "authors", authors);
cJSON_AddNumberToObject(filter, "limit", 300);
- nostr_pool_subscription_t* skill_sub = nostr_relay_pool_subscribe(
- g_pool,
- (const char**)g_cfg->relays,
- g_cfg->relay_count,
- filter,
- on_self_skill_event,
- on_self_skill_eose,
- NULL,
- 0,
- 0,
- NOSTR_POOL_EOSE_FULL_SET,
- 30,
- 120);
-
- cJSON_Delete(filter);
- if (!skill_sub) {
+ pthread_mutex_lock(&g_subscription_mutex);
+ managed_subscriptions_init_defaults();
+ if (managed_subscription_register_locked(MANAGED_SUB_SELF_SKILLS,
+ filter,
+ on_self_skill_event,
+ on_self_skill_eose,
+ NULL,
+ 0,
+ 0,
+ NOSTR_POOL_EOSE_FULL_SET,
+ 30,
+ 120,
+ 1) != 0) {
rc = -1;
}
+ pthread_mutex_unlock(&g_subscription_mutex);
+ cJSON_Delete(filter);
- if (g_cfg->admin.pubkey[0] != '\0' && strcmp(g_cfg->admin.pubkey, g_cfg->keys.public_key_hex) != 0) {
- cJSON* admin_filter = cJSON_CreateObject();
+ int admin_enabled = (g_cfg->admin.pubkey[0] != '\0' && strcmp(g_cfg->admin.pubkey, g_cfg->keys.public_key_hex) != 0) ? 1 : 0;
+ cJSON* admin_filter = NULL;
+ if (admin_enabled) {
+ admin_filter = cJSON_CreateObject();
cJSON* admin_kinds = cJSON_CreateArray();
cJSON* admin_authors = cJSON_CreateArray();
if (!admin_filter || !admin_kinds || !admin_authors) {
@@ -2005,27 +2427,26 @@ int nostr_handler_subscribe_self_skills(void) {
cJSON_AddItemToArray(admin_authors, cJSON_CreateString(g_cfg->admin.pubkey));
cJSON_AddItemToObject(admin_filter, "authors", admin_authors);
cJSON_AddNumberToObject(admin_filter, "limit", 150);
-
- nostr_pool_subscription_t* admin_skill_sub = nostr_relay_pool_subscribe(
- g_pool,
- (const char**)g_cfg->relays,
- g_cfg->relay_count,
- admin_filter,
- on_self_skill_event,
- on_eose,
- NULL,
- 0,
- 0,
- NOSTR_POOL_EOSE_FULL_SET,
- 30,
- 120);
-
- cJSON_Delete(admin_filter);
- if (!admin_skill_sub) {
- rc = -1;
- }
}
+ pthread_mutex_lock(&g_subscription_mutex);
+ managed_subscriptions_init_defaults();
+ if (managed_subscription_register_locked(MANAGED_SUB_ADMIN_SKILLS,
+ admin_filter,
+ on_self_skill_event,
+ on_eose,
+ NULL,
+ 0,
+ 0,
+ NOSTR_POOL_EOSE_FULL_SET,
+ 30,
+ 120,
+ admin_enabled) != 0) {
+ rc = -1;
+ }
+ pthread_mutex_unlock(&g_subscription_mutex);
+ cJSON_Delete(admin_filter);
+
if (rc == 0) {
DEBUG_INFO("[didactyl] self/admin skill subscriptions active (self=%.16s..., admin=%.16s...)",
g_cfg->keys.public_key_hex,
@@ -2050,12 +2471,16 @@ int nostr_handler_subscribe_dms(dm_callback_t callback, void* user_data) {
return -1;
}
- int subscribed_any = 0;
+ int rc = 0;
+ cJSON* filter4 = NULL;
if (need_kind4) {
- cJSON* filter4 = cJSON_CreateObject();
- cJSON* kinds4 = cJSON_CreateArray();
- cJSON* p_values4 = cJSON_CreateArray();
+ cJSON* kinds4 = NULL;
+ cJSON* p_values4 = NULL;
+
+ filter4 = cJSON_CreateObject();
+ kinds4 = cJSON_CreateArray();
+ p_values4 = cJSON_CreateArray();
if (!filter4 || !kinds4 || !p_values4) {
cJSON_Delete(filter4);
cJSON_Delete(kinds4);
@@ -2069,44 +2494,18 @@ int nostr_handler_subscribe_dms(dm_callback_t callback, void* user_data) {
cJSON_AddItemToObject(filter4, "#p", p_values4);
cJSON_AddNumberToObject(filter4, "since", (double)g_start_time);
cJSON_AddNumberToObject(filter4, "limit", 100);
-
- {
- char* filter_str = cJSON_PrintUnformatted(filter4);
- DEBUG_TRACE("[didactyl] DEBUG DM subscription filter kind4: %s", filter_str ? filter_str : "");
- free(filter_str);
- }
-
- nostr_pool_subscription_t* sub4 = nostr_relay_pool_subscribe(
- g_pool,
- (const char**)g_cfg->relays,
- g_cfg->relay_count,
- filter4,
- on_event,
- on_eose,
- NULL,
- 0,
- 0,
- NOSTR_POOL_EOSE_FULL_SET,
- 30,
- 120);
-
- cJSON_Delete(filter4);
- if (!sub4) {
- fprintf(stderr, "[didactyl] kind4 DM subscription failed\n");
- return -1;
- }
-
- subscribed_any = 1;
- DEBUG_TRACE("[didactyl] DEBUG kind4 DM subscription sub=%p", (void*)sub4);
}
+ cJSON* filter1059 = NULL;
if (need_kind1059) {
- time_t since_1059 = g_start_time;
+ cJSON* kinds1059 = NULL;
+ cJSON* p_values1059 = NULL;
- cJSON* filter1059 = cJSON_CreateObject();
- cJSON* kinds1059 = cJSON_CreateArray();
- cJSON* p_values1059 = cJSON_CreateArray();
+ filter1059 = cJSON_CreateObject();
+ kinds1059 = cJSON_CreateArray();
+ p_values1059 = cJSON_CreateArray();
if (!filter1059 || !kinds1059 || !p_values1059) {
+ cJSON_Delete(filter4);
cJSON_Delete(filter1059);
cJSON_Delete(kinds1059);
cJSON_Delete(p_values1059);
@@ -2117,41 +2516,46 @@ int nostr_handler_subscribe_dms(dm_callback_t callback, void* user_data) {
cJSON_AddItemToObject(filter1059, "kinds", kinds1059);
cJSON_AddItemToArray(p_values1059, cJSON_CreateString(g_cfg->keys.public_key_hex));
cJSON_AddItemToObject(filter1059, "#p", p_values1059);
- cJSON_AddNumberToObject(filter1059, "since", (double)since_1059);
+ cJSON_AddNumberToObject(filter1059, "since", (double)g_start_time);
cJSON_AddNumberToObject(filter1059, "limit", 400);
-
- {
- char* filter_str = cJSON_PrintUnformatted(filter1059);
- DEBUG_TRACE("[didactyl] DEBUG DM subscription filter kind1059: %s", filter_str ? filter_str : "");
- DEBUG_TRACE("[didactyl] DEBUG kind1059 since=%ld start=%ld", (long)since_1059, (long)g_start_time);
- free(filter_str);
- }
-
- nostr_pool_subscription_t* sub1059 = nostr_relay_pool_subscribe(
- g_pool,
- (const char**)g_cfg->relays,
- g_cfg->relay_count,
- filter1059,
- on_event,
- on_eose,
- NULL,
- 0,
- 0,
- NOSTR_POOL_EOSE_FULL_SET,
- 30,
- 120);
-
- cJSON_Delete(filter1059);
- if (!sub1059) {
- fprintf(stderr, "[didactyl] kind1059 DM subscription failed\n");
- return -1;
- }
-
- subscribed_any = 1;
- DEBUG_TRACE("[didactyl] DEBUG kind1059 DM subscription sub=%p", (void*)sub1059);
}
- if (!subscribed_any) {
+ pthread_mutex_lock(&g_subscription_mutex);
+ managed_subscriptions_init_defaults();
+
+ if (managed_subscription_register_locked(MANAGED_SUB_DM_KIND4,
+ need_kind4 ? filter4 : NULL,
+ on_event,
+ on_eose,
+ NULL,
+ 0,
+ 0,
+ NOSTR_POOL_EOSE_FULL_SET,
+ 30,
+ 120,
+ need_kind4) != 0) {
+ rc = -1;
+ }
+
+ if (managed_subscription_register_locked(MANAGED_SUB_DM_KIND1059,
+ need_kind1059 ? filter1059 : NULL,
+ on_event,
+ on_eose,
+ NULL,
+ 0,
+ 0,
+ NOSTR_POOL_EOSE_FULL_SET,
+ 30,
+ 120,
+ need_kind1059) != 0) {
+ rc = -1;
+ }
+
+ pthread_mutex_unlock(&g_subscription_mutex);
+ cJSON_Delete(filter4);
+ cJSON_Delete(filter1059);
+
+ if (rc != 0) {
return -1;
}
@@ -2553,7 +2957,12 @@ int nostr_handler_reconcile_startup_events(void) {
}
if (!g_system_context) {
- g_system_context = strdup("You are Didactyl, a sovereign AI agent living on Nostr.");
+ char default_ctx[512];
+ snprintf(default_ctx,
+ sizeof(default_ctx),
+ "You are %s, a sovereign AI agent living on Nostr.",
+ g_startup_display_name[0] != '\0' ? g_startup_display_name : "the agent");
+ g_system_context = strdup(default_ctx);
}
for (int relay_index = 0; relay_index < g_cfg->relay_count; relay_index++) {
@@ -2748,6 +3157,33 @@ char* nostr_handler_get_self_skill_events_json(void) {
return json ? json : strdup("[]");
}
+int nostr_handler_is_event_in_self_cache(const char* event_id_hex) {
+ if (!event_id_hex || strlen(event_id_hex) != 64U) {
+ return 0;
+ }
+
+ int found = 0;
+
+ pthread_mutex_lock(&g_self_skill_mutex);
+ if (g_self_skill_events && cJSON_IsArray(g_self_skill_events)) {
+ int n = cJSON_GetArraySize(g_self_skill_events);
+ for (int i = 0; i < n; i++) {
+ cJSON* ev = cJSON_GetArrayItem(g_self_skill_events, i);
+ cJSON* id = ev ? cJSON_GetObjectItemCaseSensitive(ev, "id") : NULL;
+ if (!id || !cJSON_IsString(id) || !id->valuestring) {
+ continue;
+ }
+ if (strcmp(id->valuestring, event_id_hex) == 0) {
+ found = 1;
+ break;
+ }
+ }
+ }
+ pthread_mutex_unlock(&g_self_skill_mutex);
+
+ return found;
+}
+
const char* nostr_handler_get_startup_display_name(void) {
return g_startup_display_name;
}
@@ -2986,6 +3422,94 @@ char* nostr_handler_relay_info_json(const char* relay_url) {
return out;
}
+char* nostr_handler_subscription_status_json(void) {
+ cJSON* root = cJSON_CreateObject();
+ cJSON* subs = cJSON_CreateArray();
+ if (!root || !subs) {
+ cJSON_Delete(root);
+ cJSON_Delete(subs);
+ return NULL;
+ }
+
+ pthread_mutex_lock(&g_subscription_mutex);
+ managed_subscriptions_init_defaults();
+
+ for (int i = 0; i < MANAGED_SUB_COUNT; i++) {
+ managed_subscription_t* sub = &g_managed_subs[i];
+
+ cJSON* item = cJSON_CreateObject();
+ if (!item) {
+ continue;
+ }
+
+ cJSON_AddStringToObject(item, "name", sub->name ? sub->name : "");
+ cJSON_AddBoolToObject(item, "enabled", sub->enabled ? 1 : 0);
+ cJSON_AddBoolToObject(item, "active", sub->handle ? 1 : 0);
+ cJSON_AddBoolToObject(item, "has_filter", sub->filter ? 1 : 0);
+ cJSON_AddNumberToObject(item, "close_on_eose", sub->close_on_eose);
+ cJSON_AddNumberToObject(item, "enable_deduplication", sub->enable_deduplication);
+ cJSON_AddNumberToObject(item, "relay_timeout_seconds", sub->relay_timeout_seconds);
+ cJSON_AddNumberToObject(item, "eose_timeout_seconds", sub->eose_timeout_seconds);
+
+ if (sub->filter) {
+ cJSON* filter_dup = cJSON_Duplicate(sub->filter, 1);
+ if (filter_dup) {
+ cJSON_AddItemToObject(item, "filter", filter_dup);
+ }
+ }
+
+ cJSON_AddItemToArray(subs, item);
+ }
+
+ pthread_mutex_unlock(&g_subscription_mutex);
+
+ cJSON_AddNumberToObject(root, "count", MANAGED_SUB_COUNT);
+ cJSON_AddItemToObject(root, "subscriptions", subs);
+
+ char* out = cJSON_PrintUnformatted(root);
+ cJSON_Delete(root);
+ return out;
+}
+
+int nostr_handler_subscription_set_json(const char* name, cJSON* filter, int enabled, int enabled_set) {
+ if (!name || name[0] == '\0') {
+ return -1;
+ }
+
+ pthread_mutex_lock(&g_subscription_mutex);
+ managed_subscriptions_init_defaults();
+
+ managed_subscription_id_t id = MANAGED_SUB_COUNT;
+ if (managed_subscription_id_from_name(name, &id) != 0) {
+ pthread_mutex_unlock(&g_subscription_mutex);
+ return -1;
+ }
+
+ managed_subscription_t* sub = &g_managed_subs[id];
+
+ cJSON* filter_to_use = sub->filter;
+ if (filter) {
+ filter_to_use = filter;
+ }
+
+ int enabled_to_use = enabled_set ? (enabled ? 1 : 0) : sub->enabled;
+
+ int rc = managed_subscription_register_locked(id,
+ filter_to_use,
+ sub->on_event,
+ sub->on_eose,
+ sub->user_data,
+ sub->close_on_eose,
+ sub->enable_deduplication,
+ sub->result_mode,
+ sub->relay_timeout_seconds,
+ sub->eose_timeout_seconds,
+ enabled_to_use);
+
+ pthread_mutex_unlock(&g_subscription_mutex);
+ return rc;
+}
+
int nostr_handler_send_dm_nip17(const char* recipient_pubkey_hex, const char* message, const char* subject) {
if (!g_cfg || !g_pool || !recipient_pubkey_hex || !message || recipient_pubkey_hex[0] == '\0' || message[0] == '\0') {
return -1;
@@ -3368,6 +3892,19 @@ int nostr_handler_poll(int timeout_ms) {
}
void nostr_handler_cleanup(void) {
+ pthread_mutex_lock(&g_subscription_mutex);
+ managed_subscriptions_init_defaults();
+ for (int i = 0; i < MANAGED_SUB_COUNT; i++) {
+ (void)managed_subscription_close_locked((managed_subscription_id_t)i);
+ cJSON_Delete(g_managed_subs[i].filter);
+ g_managed_subs[i].filter = NULL;
+ g_managed_subs[i].enabled = 0;
+ g_managed_subs[i].on_event = NULL;
+ g_managed_subs[i].on_eose = NULL;
+ g_managed_subs[i].user_data = NULL;
+ }
+ pthread_mutex_unlock(&g_subscription_mutex);
+
if (g_pool) {
nostr_relay_pool_destroy(g_pool);
}
diff --git a/src/nostr_handler.h b/src/nostr_handler.h
index ef48595..5d89562 100644
--- a/src/nostr_handler.h
+++ b/src/nostr_handler.h
@@ -63,6 +63,7 @@ void nostr_handler_refresh_relay_statuses(void);
int nostr_handler_sync_relays_from_config(void);
const char* nostr_handler_get_system_context(void);
char* nostr_handler_get_self_skill_events_json(void);
+int nostr_handler_is_event_in_self_cache(const char* event_id_hex);
const char* nostr_handler_get_startup_display_name(void);
int nostr_handler_connected_relay_count(void);
char* nostr_handler_get_admin_kind0_context(void);
@@ -76,6 +77,8 @@ char* nostr_handler_get_agent_kind1_notes_context(void);
int nostr_handler_is_wot_contact(const char* pubkey_hex);
char* nostr_handler_relay_status_json(void);
char* nostr_handler_relay_info_json(const char* relay_url);
+char* nostr_handler_subscription_status_json(void);
+int nostr_handler_subscription_set_json(const char* name, cJSON* filter, int enabled, int enabled_set);
int nostr_handler_send_dm_nip17(const char* recipient_pubkey_hex, const char* message, const char* subject);
char* nostr_handler_get_dm_history_json(const char* peer_pubkey_hex, int limit);
void nostr_handler_cleanup(void);
diff --git a/src/setup_wizard.c b/src/setup_wizard.c
index a41deef..31324a2 100644
--- a/src/setup_wizard.c
+++ b/src/setup_wizard.c
@@ -11,11 +11,16 @@
#include
#include
#include
+#include
+#include
+#include
+#include
#include "cjson/cJSON.h"
#include "llm.h"
#include "main.h"
#include "nostr_handler.h"
+#include "default_events.h"
#include "../../nostr_core_lib/nostr_core/nostr_core.h"
#define ANSI_UNDERLINE "\x1b[4m"
@@ -321,20 +326,15 @@ static void free_relays_only(didactyl_config_t* cfg) {
static int set_default_relays(didactyl_config_t* cfg) {
if (!cfg) return -1;
- static const char* defaults[] = {
- "wss://relay.damus.io",
- "wss://nos.lol",
- "wss://relay.primal.net"
- };
free_relays_only(cfg);
- cfg->relay_count = (int)(sizeof(defaults) / sizeof(defaults[0]));
+ cfg->relay_count = DIDACTYL_DEFAULT_RELAY_COUNT;
cfg->relays = (char**)calloc((size_t)cfg->relay_count, sizeof(char*));
if (!cfg->relays) {
cfg->relay_count = 0;
return -1;
}
for (int i = 0; i < cfg->relay_count; i++) {
- cfg->relays[i] = strdup(defaults[i]);
+ cfg->relays[i] = strdup(DIDACTYL_DEFAULT_RELAYS[i]);
if (!cfg->relays[i]) {
free_relays_only(cfg);
return -1;
@@ -369,131 +369,161 @@ static int build_kind10002_tags_json(const didactyl_config_t* cfg, char** out_ta
return 0;
}
-static int sync_startup_kind0_event(didactyl_config_t* cfg, const char* name) {
- if (!cfg || !name || name[0] == '\0') return -1;
-
- cJSON* profile = cJSON_CreateObject();
- if (!profile) return -1;
- cJSON_AddStringToObject(profile, "name", name);
- cJSON_AddStringToObject(profile, "display_name", name);
-
- char* content_json = cJSON_PrintUnformatted(profile);
- cJSON_Delete(profile);
- if (!content_json) return -1;
-
- if (!cfg->startup_events || cfg->startup_event_count <= 0) {
- cfg->startup_events = (startup_event_t*)calloc(1U, sizeof(startup_event_t));
- if (!cfg->startup_events) {
- free(content_json);
- return -1;
- }
- cfg->startup_event_count = 1;
- cfg->startup_events[0].kind = 0;
- cfg->startup_events[0].content = content_json;
- cfg->startup_events[0].tags_json = strdup("[]");
- if (!cfg->startup_events[0].tags_json) {
- free(cfg->startup_events[0].content);
- free(cfg->startup_events);
- cfg->startup_events = NULL;
- cfg->startup_event_count = 0;
- return -1;
- }
- return 0;
+static int upsert_startup_event(didactyl_config_t* cfg, int kind, const char* content, const char* tags_json) {
+ if (!cfg || !content || !tags_json) {
+ return -1;
}
for (int i = 0; i < cfg->startup_event_count; i++) {
- if (cfg->startup_events[i].kind == 0) {
- free(cfg->startup_events[i].content);
- cfg->startup_events[i].content = content_json;
- if (!cfg->startup_events[i].tags_json) {
- cfg->startup_events[i].tags_json = strdup("[]");
- if (!cfg->startup_events[i].tags_json) {
- return -1;
- }
- }
- return 0;
+ startup_event_t* se = &cfg->startup_events[i];
+ if (se->kind != kind) {
+ continue;
}
+
+ char* content_dup = strdup(content);
+ char* tags_dup = strdup(tags_json);
+ if (!content_dup || !tags_dup) {
+ free(content_dup);
+ free(tags_dup);
+ return -1;
+ }
+
+ free(se->content);
+ free(se->tags_json);
+ se->content = content_dup;
+ se->tags_json = tags_dup;
+ return 0;
}
startup_event_t* grown = (startup_event_t*)realloc(cfg->startup_events,
(size_t)(cfg->startup_event_count + 1) * sizeof(startup_event_t));
if (!grown) {
- free(content_json);
+ return -1;
+ }
+ cfg->startup_events = grown;
+
+ startup_event_t* se = &cfg->startup_events[cfg->startup_event_count];
+ memset(se, 0, sizeof(*se));
+ se->kind = kind;
+ se->content = strdup(content);
+ se->tags_json = strdup(tags_json);
+ if (!se->content || !se->tags_json) {
+ free(se->content);
+ free(se->tags_json);
+ se->content = NULL;
+ se->tags_json = NULL;
return -1;
}
- cfg->startup_events = grown;
- startup_event_t* se = &cfg->startup_events[cfg->startup_event_count];
- memset(se, 0, sizeof(*se));
- se->kind = 0;
- se->content = content_json;
- se->tags_json = strdup("[]");
- if (!se->tags_json) {
- free(se->content);
- se->content = NULL;
- return -1;
- }
cfg->startup_event_count++;
return 0;
}
-static int sync_startup_kind10002_event(didactyl_config_t* cfg) {
- if (!cfg) return -1;
-
- char* tags_json = NULL;
- if (build_kind10002_tags_json(cfg, &tags_json) != 0) {
+static int apply_default_startup_events(didactyl_config_t* cfg, const char* agent_name) {
+ if (!cfg || !agent_name || agent_name[0] == '\0' || cfg->admin.pubkey[0] == '\0') {
return -1;
}
- if (!cfg->startup_events || cfg->startup_event_count <= 0) {
- cfg->startup_events = (startup_event_t*)calloc(1U, sizeof(startup_event_t));
- if (!cfg->startup_events) {
- free(tags_json);
- return -1;
- }
- cfg->startup_event_count = 1;
- cfg->startup_events[0].kind = 10002;
- cfg->startup_events[0].content = strdup("");
- cfg->startup_events[0].tags_json = tags_json;
- if (!cfg->startup_events[0].content) {
- free(cfg->startup_events[0].tags_json);
- free(cfg->startup_events);
- cfg->startup_events = NULL;
- cfg->startup_event_count = 0;
- return -1;
- }
- return 0;
- }
+ for (int i = 0; i < DIDACTYL_DEFAULT_STARTUP_EVENT_KIND_COUNT; i++) {
+ int kind = DIDACTYL_DEFAULT_STARTUP_EVENT_KINDS[i];
- for (int i = 0; i < cfg->startup_event_count; i++) {
- if (cfg->startup_events[i].kind == 10002) {
- free(cfg->startup_events[i].tags_json);
- cfg->startup_events[i].tags_json = tags_json;
- if (!cfg->startup_events[i].content) {
- cfg->startup_events[i].content = strdup("");
+ if (kind == DIDACTYL_STARTUP_KIND_PROFILE) {
+ int needed = snprintf(NULL,
+ 0,
+ DIDACTYL_DEFAULT_KIND0_PROFILE_JSON_TEMPLATE,
+ agent_name,
+ agent_name);
+ if (needed <= 0) {
+ return -1;
}
- return 0;
+ char* content_json = (char*)malloc((size_t)needed + 1U);
+ if (!content_json) {
+ return -1;
+ }
+ snprintf(content_json,
+ (size_t)needed + 1U,
+ DIDACTYL_DEFAULT_KIND0_PROFILE_JSON_TEMPLATE,
+ agent_name,
+ agent_name);
+
+ int rc = upsert_startup_event(cfg, kind, content_json, "[]");
+ free(content_json);
+ if (rc != 0) {
+ return -1;
+ }
+ continue;
+ }
+
+ if (kind == DIDACTYL_STARTUP_KIND_CONTACTS) {
+ int needed = snprintf(NULL, 0, DIDACTYL_DEFAULT_KIND3_TAGS_JSON_TEMPLATE, cfg->admin.pubkey);
+ if (needed <= 0) {
+ return -1;
+ }
+ char* tags_json = (char*)malloc((size_t)needed + 1U);
+ if (!tags_json) {
+ return -1;
+ }
+ snprintf(tags_json, (size_t)needed + 1U, DIDACTYL_DEFAULT_KIND3_TAGS_JSON_TEMPLATE, cfg->admin.pubkey);
+
+ int rc = upsert_startup_event(cfg, kind, DIDACTYL_DEFAULT_KIND3_CONTENT, tags_json);
+ free(tags_json);
+ if (rc != 0) {
+ return -1;
+ }
+ continue;
+ }
+
+ if (kind == DIDACTYL_STARTUP_KIND_RELAYS) {
+ char* tags_json = NULL;
+ if (build_kind10002_tags_json(cfg, &tags_json) != 0 || !tags_json) {
+ free(tags_json);
+ return -1;
+ }
+
+ int rc = upsert_startup_event(cfg, kind, DIDACTYL_DEFAULT_KIND10002_CONTENT, tags_json);
+ free(tags_json);
+ if (rc != 0) {
+ return -1;
+ }
+ continue;
}
}
- startup_event_t* grown = (startup_event_t*)realloc(cfg->startup_events,
- (size_t)(cfg->startup_event_count + 1) * sizeof(startup_event_t));
- if (!grown) {
- free(tags_json);
+ return 0;
+}
+
+static int configure_default_skill_for_agent(didactyl_config_t* cfg, const char* agent_name) {
+ if (!cfg || !agent_name || agent_name[0] == '\0') {
return -1;
}
- cfg->startup_events = grown;
- startup_event_t* se = &cfg->startup_events[cfg->startup_event_count];
- memset(se, 0, sizeof(*se));
- se->kind = 10002;
- se->content = strdup("");
- se->tags_json = tags_json;
- if (!se->content) {
- free(se->tags_json);
- se->tags_json = NULL;
+
+ free(cfg->default_skill.content);
+ cfg->default_skill.content = NULL;
+ free(cfg->default_skill.tags_json);
+ cfg->default_skill.tags_json = NULL;
+
+ cfg->default_skill.kind = DIDACTYL_DEFAULT_SKILL_KIND;
+ snprintf(cfg->default_skill.d_tag, sizeof(cfg->default_skill.d_tag), "%s", DIDACTYL_DEFAULT_SKILL_D_TAG);
+
+ int needed = snprintf(NULL, 0, DIDACTYL_DEFAULT_SKILL_TEMPLATE, agent_name, agent_name);
+ if (needed <= 0) {
return -1;
}
- cfg->startup_event_count++;
+
+ cfg->default_skill.content = (char*)malloc((size_t)needed + 1U);
+ if (!cfg->default_skill.content) {
+ return -1;
+ }
+
+ snprintf(cfg->default_skill.content, (size_t)needed + 1U, DIDACTYL_DEFAULT_SKILL_TEMPLATE, agent_name, agent_name);
+
+ cfg->default_skill.tags_json = strdup(DIDACTYL_DEFAULT_SKILL_TAGS_JSON);
+ if (!cfg->default_skill.tags_json) {
+ free(cfg->default_skill.content);
+ cfg->default_skill.content = NULL;
+ return -1;
+ }
+
return 0;
}
@@ -1010,7 +1040,7 @@ static int prompt_model_selection(const cJSON* models,
static int prompt_admin_pubkey(didactyl_config_t* cfg) {
char input[WIZARD_LINE_MAX];
for (;;) {
- render_wizard_page_header("Step 2 of 7", "New Agent Setup -- Administrator");
+ render_wizard_page_header("Step 3 of 7", "New Agent Setup -- Administrator");
if (read_line_prompt(" Enter the admin's Nostr public key (npub1... or hex):\n> ", input, sizeof(input)) != 0) {
return -1;
}
@@ -1028,7 +1058,7 @@ static int prompt_admin_pubkey(didactyl_config_t* cfg) {
static int prompt_llm_config(didactyl_config_t* cfg) {
for (;;) {
- render_wizard_page_header("Step 4 of 7", "New Agent Setup -- LLM Provider");
+ render_wizard_page_header("Step 5 of 7", "New Agent Setup -- LLM Provider");
print_option('1', " ppq.ai");
print_option('2', " Routstr");
print_option('3', " OpenAI-compatible");
@@ -1119,25 +1149,7 @@ static int prompt_llm_config(didactyl_config_t* cfg) {
cJSON_Delete(models);
int max_tokens = cfg->llm.max_tokens > 0 ? cfg->llm.max_tokens : 512;
- char max_tokens_s[32] = {0};
- char prompt[256];
- snprintf(prompt, sizeof(prompt), " Max Tokens [%d]: ", max_tokens);
- if (read_line_prompt(prompt, max_tokens_s, sizeof(max_tokens_s)) != 0) return -1;
- if (line_is_quit(max_tokens_s)) return -1;
- if (max_tokens_s[0] != '\0') {
- int mt = atoi(max_tokens_s);
- if (mt > 0) max_tokens = mt;
- }
-
double temperature = cfg->llm.temperature > 0.0 ? cfg->llm.temperature : 0.7;
- char temperature_s[32] = {0};
- snprintf(prompt, sizeof(prompt), " Temperature [%.1f]: ", temperature);
- if (read_line_prompt(prompt, temperature_s, sizeof(temperature_s)) != 0) return -1;
- if (line_is_quit(temperature_s)) return -1;
- if (temperature_s[0] != '\0') {
- double t = atof(temperature_s);
- if (t >= 0.0 && t <= 2.0) temperature = t;
- }
snprintf(cfg->llm.provider, sizeof(cfg->llm.provider), "%s", provider);
snprintf(cfg->llm.base_url, sizeof(cfg->llm.base_url), "%s", base_url);
@@ -1171,7 +1183,7 @@ static int prompt_relay_configuration(didactyl_config_t* cfg) {
char input[WIZARD_LINE_MAX];
for (;;) {
- render_wizard_page_header("Step 5 of 7", "New Agent Setup -- Relay Configuration");
+ render_wizard_page_header("Step 6 of 7", "New Agent Setup -- Relay Configuration");
fprintf(stderr, " Current relays:\n");
for (int i = 0; i < cfg->relay_count; i++) {
fprintf(stderr, " %d. %s\n", i + 1, cfg->relays[i] ? cfg->relays[i] : "");
@@ -1223,53 +1235,308 @@ static int prompt_relay_configuration(didactyl_config_t* cfg) {
}
}
-static int write_genesis_file(const didactyl_config_t* cfg, const char* path, int include_nsec) {
- if (!cfg || !path || path[0] == '\0') return -1;
+static int ensure_dir_exists_local(const char* path) {
+ if (!path || path[0] == '\0') return -1;
+ if (mkdir(path, 0755) == 0 || errno == EEXIST) return 0;
+ return -1;
+}
- FILE* fp = fopen(path, "w");
- if (!fp) return -1;
-
- fprintf(fp, "{\n");
- fprintf(fp, " \"key\": {\n");
- if (include_nsec) {
- fprintf(fp, " \"nsec\": \"%s\"\n", cfg->keys.nsec);
+static int run_command_local(char* const argv[]) {
+ pid_t pid = fork();
+ if (pid < 0) return -1;
+ if (pid == 0) {
+ execvp(argv[0], argv);
+ _exit(127);
}
- fprintf(fp, " },\n");
- fprintf(fp, " \"admin\": {\n");
- fprintf(fp, " \"pubkey\": \"%s\"\n", cfg->admin.pubkey);
- fprintf(fp, " },\n");
- fprintf(fp, " \"dm_protocol\": \"nip04\",\n");
- fprintf(fp, " \"llm\": {\n");
- fprintf(fp, " \"provider\": \"%s\",\n", cfg->llm.provider[0] ? cfg->llm.provider : "openai");
- fprintf(fp, " \"api_key\": \"%s\",\n", cfg->llm.api_key);
- fprintf(fp, " \"model\": \"%s\",\n", cfg->llm.model);
- fprintf(fp, " \"base_url\": \"%s\",\n", cfg->llm.base_url);
- fprintf(fp, " \"max_tokens\": %d,\n", cfg->llm.max_tokens > 0 ? cfg->llm.max_tokens : 512);
- fprintf(fp, " \"temperature\": %.2f\n", cfg->llm.temperature > 0.0 ? cfg->llm.temperature : 0.7);
- fprintf(fp, " },\n");
- fprintf(fp, " \"api\": {\n");
- fprintf(fp, " \"enabled\": true,\n");
- fprintf(fp, " \"port\": %d,\n", cfg->api.port > 0 ? cfg->api.port : 8484);
- fprintf(fp, " \"bind_address\": \"%s\"\n", cfg->api.bind_address[0] ? cfg->api.bind_address : "127.0.0.1");
- fprintf(fp, " },\n");
- fprintf(fp, " \"startup_events\": [\n");
- fprintf(fp, " {\n");
- fprintf(fp, " \"kind\": 10002,\n");
- fprintf(fp, " \"content\": \"\",\n");
- fprintf(fp, " \"tags\": [\n");
- for (int i = 0; i < cfg->relay_count; i++) {
- fprintf(fp, " [\"r\", \"%s\"]%s\n", cfg->relays[i], (i + 1 < cfg->relay_count) ? "," : "");
- }
- fprintf(fp, " ]\n");
- fprintf(fp, " }\n");
- fprintf(fp, " ]\n");
- fprintf(fp, "}\n");
- fclose(fp);
+ int status = 0;
+ if (waitpid(pid, &status, 0) < 0) return -1;
+ if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) return -1;
return 0;
}
-static int maybe_prompt_write_path(char* path_out, size_t path_out_size) {
+static int copy_file_local(const char* src, const char* dst) {
+ if (!src || !dst || src[0] == '\0' || dst[0] == '\0') return -1;
+
+ FILE* in = fopen(src, "rb");
+ if (!in) return -1;
+
+ FILE* out = fopen(dst, "wb");
+ if (!out) {
+ fclose(in);
+ return -1;
+ }
+
+ char buf[16384];
+ size_t n = 0;
+ while ((n = fread(buf, 1, sizeof(buf), in)) > 0) {
+ if (fwrite(buf, 1, n, out) != n) {
+ fclose(in);
+ fclose(out);
+ return -1;
+ }
+ }
+
+ fclose(in);
+ fclose(out);
+ return 0;
+}
+
+static void sanitize_service_user_from_name(const char* name, char* out, size_t out_size) {
+ if (!out || out_size == 0) return;
+ out[0] = '\0';
+
+ const char* src = (name && name[0] != '\0') ? name : "didactyl";
+ size_t j = 0;
+ for (size_t i = 0; src[i] != '\0' && j + 1 < out_size; i++) {
+ char c = (char)tolower((unsigned char)src[i]);
+ if ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9')) {
+ out[j++] = c;
+ } else if ((c == ' ' || c == '_' || c == '-') && j > 0 && out[j - 1] != '-') {
+ out[j++] = '-';
+ }
+ if (j >= 31) break;
+ }
+
+ while (j > 0 && out[j - 1] == '-') j--;
+ out[j] = '\0';
+
+ if (out[0] == '\0' || !(out[0] >= 'a' && out[0] <= 'z')) {
+ snprintf(out, out_size, "didactyl");
+ }
+}
+
+
+static const char* detect_ca_bundle_path_local(void) {
+ const char* env = getenv("SSL_CERT_FILE");
+ if (env && env[0] != '\0' && access(env, R_OK) == 0) {
+ return env;
+ }
+ static const char* candidates[] = {
+ "/etc/ssl/certs/ca-certificates.crt",
+ "/etc/ssl/cert.pem",
+ "/etc/pki/tls/certs/ca-bundle.crt",
+ "/etc/ssl/ca-bundle.pem"
+ };
+ for (size_t i = 0; i < sizeof(candidates) / sizeof(candidates[0]); i++) {
+ if (access(candidates[i], R_OK) == 0) {
+ return candidates[i];
+ }
+ }
+ return NULL;
+}
+
+static int write_sudoers_file(const char* service_user) {
+ if (!service_user || service_user[0] == '\0') return -1;
+
+ char sudoers_path[PATH_MAX] = {0};
+ snprintf(sudoers_path, sizeof(sudoers_path), "/etc/sudoers.d/%s", service_user);
+
+ FILE* fp = fopen(sudoers_path, "w");
+ if (!fp) {
+ fprintf(stderr, "%sFailed to write sudoers file at %s.%s\n", ANSI_RED, sudoers_path, ANSI_RESET);
+ return -1;
+ }
+
+ fprintf(fp,
+ "# Didactyl agent — scoped system maintenance privileges\n"
+ "# Generated by didactyl setup wizard\n"
+ "\n"
+ "# Service management\n"
+ "%s ALL=(ALL) NOPASSWD: /usr/bin/systemctl status *\n"
+ "%s ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart *\n"
+ "%s ALL=(ALL) NOPASSWD: /usr/bin/systemctl start *\n"
+ "%s ALL=(ALL) NOPASSWD: /usr/bin/systemctl stop *\n"
+ "%s ALL=(ALL) NOPASSWD: /usr/bin/systemctl enable *\n"
+ "%s ALL=(ALL) NOPASSWD: /usr/bin/systemctl disable *\n"
+ "%s ALL=(ALL) NOPASSWD: /usr/bin/systemctl is-active *\n"
+ "%s ALL=(ALL) NOPASSWD: /usr/bin/systemctl is-enabled *\n"
+ "%s ALL=(ALL) NOPASSWD: /usr/bin/systemctl list-units *\n"
+ "%s ALL=(ALL) NOPASSWD: /usr/bin/systemctl daemon-reload\n"
+ "\n"
+ "# Log inspection\n"
+ "%s ALL=(ALL) NOPASSWD: /usr/bin/journalctl *\n",
+ service_user, service_user, service_user, service_user,
+ service_user, service_user, service_user, service_user,
+ service_user, service_user, service_user);
+ fclose(fp);
+
+ if (chmod(sudoers_path, 0440) != 0) {
+ fprintf(stderr, "%sFailed to chmod sudoers file at %s.%s\n", ANSI_RED, sudoers_path, ANSI_RESET);
+ return -1;
+ }
+
+ return 0;
+}
+
+static int install_system_service_with_dedicated_user(const didactyl_config_t* cfg, const char* agent_name) {
+ if (!cfg || cfg->keys.nsec[0] == '\0') return -1;
+
+ if (geteuid() != 0) {
+ fprintf(stderr, "%sSystem install requires root (run with sudo).%s\n", ANSI_RED, ANSI_RESET);
+ return -1;
+ }
+
+ /* --- Derive service username from agent name --- */
+ char service_user[64] = {0};
+ sanitize_service_user_from_name(agent_name, service_user, sizeof(service_user));
+
+ /* --- Create dedicated user if it doesn't exist --- */
+ struct passwd* pw = getpwnam(service_user);
+ if (!pw) {
+ fprintf(stderr, " Creating system user '%s'...\n", service_user);
+ char comment[128] = {0};
+ snprintf(comment, sizeof(comment), "Didactyl Nostr Agent (%s)", agent_name);
+ char* useradd_argv[] = {"useradd", "-m", "-s", "/bin/bash", "-c", comment, service_user, NULL};
+ if (run_command_local(useradd_argv) != 0) {
+ fprintf(stderr, "%sFailed to create system user '%s'.%s\n", ANSI_RED, service_user, ANSI_RESET);
+ return -1;
+ }
+ pw = getpwnam(service_user);
+ if (!pw) {
+ fprintf(stderr, "%sCreated user but failed to resolve passwd entry.%s\n", ANSI_RED, ANSI_RESET);
+ return -1;
+ }
+ } else {
+ fprintf(stderr, " User '%s' already exists, reusing.\n", service_user);
+ }
+
+ const char* home = (pw->pw_dir && pw->pw_dir[0] != '\0') ? pw->pw_dir : NULL;
+ if (!home) {
+ fprintf(stderr, "%sUnable to determine service user home directory.%s\n", ANSI_RED, ANSI_RESET);
+ return -1;
+ }
+
+ if (ensure_dir_exists_local(home) != 0) {
+ fprintf(stderr, "%sUnable to create/access home directory: %s%s\n", ANSI_RED, home, ANSI_RESET);
+ return -1;
+ }
+
+ /* --- Copy binary --- */
+ char self_path[PATH_MAX] = {0};
+ ssize_t self_len = readlink("/proc/self/exe", self_path, sizeof(self_path) - 1);
+ if (self_len <= 0) {
+ fprintf(stderr, "%sUnable to resolve running binary path.%s\n", ANSI_RED, ANSI_RESET);
+ return -1;
+ }
+ self_path[self_len] = '\0';
+
+ char bin_path[PATH_MAX] = {0};
+ snprintf(bin_path, sizeof(bin_path), "%s/didactyl", home);
+ fprintf(stderr, " Copying binary to %s...\n", bin_path);
+ if (copy_file_local(self_path, bin_path) != 0) {
+ fprintf(stderr, "%sFailed to copy binary to %s.%s\n", ANSI_RED, bin_path, ANSI_RESET);
+ return -1;
+ }
+ if (chmod(bin_path, 0755) != 0) {
+ fprintf(stderr, "%sFailed to chmod binary at %s.%s\n", ANSI_RED, bin_path, ANSI_RESET);
+ return -1;
+ }
+
+ /* --- Set ownership on home directory and installed binary --- */
+ if (chown(home, pw->pw_uid, pw->pw_gid) != 0 ||
+ chmod(home, 0750) != 0 ||
+ chown(bin_path, pw->pw_uid, pw->pw_gid) != 0) {
+ fprintf(stderr, "%sFailed to set ownership/permissions for installed files.%s\n", ANSI_RED, ANSI_RESET);
+ return -1;
+ }
+
+ /* --- Write sudoers file for scoped sudo privileges --- */
+ fprintf(stderr, " Writing sudoers file for '%s'...\n", service_user);
+ if (write_sudoers_file(service_user) != 0) {
+ fprintf(stderr, "%sWarning: Failed to write sudoers file. Agent will not have sudo access.%s\n",
+ ANSI_YELLOW, ANSI_RESET);
+ /* Non-fatal — agent can still run without sudo */
+ }
+
+ /* --- Detect CA bundle for TLS --- */
+ const char* ca_bundle = detect_ca_bundle_path_local();
+
+ /* --- Write systemd unit file --- */
+ char service_name[96] = {0};
+ snprintf(service_name, sizeof(service_name), "%s.service", service_user);
+
+ char service_path[PATH_MAX] = {0};
+ snprintf(service_path, sizeof(service_path), "/etc/systemd/system/%s", service_name);
+ fprintf(stderr, " Writing systemd unit to %s...\n", service_path);
+ FILE* svc_fp = fopen(service_path, "w");
+ if (!svc_fp) {
+ fprintf(stderr, "%sFailed to write service file at %s.%s\n", ANSI_RED, service_path, ANSI_RESET);
+ return -1;
+ }
+
+ fprintf(svc_fp,
+ "[Unit]\n"
+ "Description=Didactyl Nostr Agent (%s)\n"
+ "After=network-online.target\n"
+ "Wants=network-online.target\n\n"
+ "[Service]\n"
+ "Type=simple\n"
+ "User=%s\n"
+ "Group=%s\n"
+ "WorkingDirectory=%s\n"
+ "ExecStart=%s --nsec %s --debug 3\n",
+ service_user,
+ service_user,
+ service_user,
+ home,
+ bin_path,
+ cfg->keys.nsec);
+
+ if (ca_bundle) {
+ fprintf(svc_fp, "Environment=SSL_CERT_FILE=%s\n", ca_bundle);
+ }
+
+ fprintf(svc_fp,
+ "SyslogIdentifier=%s\n"
+ "NoNewPrivileges=false\n"
+ "ProtectSystem=strict\n"
+ "ReadWritePaths=%s\n"
+ "ProtectHome=false\n"
+ "PrivateTmp=yes\n"
+ "Restart=on-failure\n"
+ "RestartSec=10\n"
+ "StandardOutput=journal\n"
+ "StandardError=journal\n\n"
+ "[Install]\n"
+ "WantedBy=multi-user.target\n",
+ service_user,
+ home);
+ fclose(svc_fp);
+
+ /* --- Enable and start the service --- */
+ fprintf(stderr, " Enabling and starting %s...\n", service_name);
+ char* reload_argv[] = {"systemctl", "daemon-reload", NULL};
+ char* enable_argv[] = {"systemctl", "enable", service_name, NULL};
+ char* start_argv[] = {"systemctl", "start", service_name, NULL};
+
+ if (run_command_local(reload_argv) != 0 ||
+ run_command_local(enable_argv) != 0 ||
+ run_command_local(start_argv) != 0) {
+ fprintf(stderr, "%sFailed to enable/start systemd service %s.%s\n", ANSI_RED, service_name, ANSI_RESET);
+ fprintf(stderr, " You can manually start with:\n");
+ fprintf(stderr, " sudo systemctl daemon-reload\n");
+ fprintf(stderr, " sudo systemctl enable %s\n", service_name);
+ fprintf(stderr, " sudo systemctl start %s\n", service_name);
+ return -1;
+ }
+
+ fprintf(stderr, "\n%sSystem service installed and started.%s\n", ANSI_YELLOW, ANSI_RESET);
+ fprintf(stderr, " Service: %s\n", service_name);
+ fprintf(stderr, " User: %s\n", service_user);
+ fprintf(stderr, " Home: %s\n", home);
+ fprintf(stderr, " Binary: %s\n", bin_path);
+ fprintf(stderr, " Nsec: embedded directly in ExecStart\n");
+ fprintf(stderr, " Sudoers: /etc/sudoers.d/%s\n", service_user);
+ fprintf(stderr, " Unit: %s\n", service_path);
+ fprintf(stderr, "\n Useful commands:\n");
+ fprintf(stderr, " sudo systemctl status %s\n", service_name);
+ fprintf(stderr, " sudo journalctl -u %s -f\n", service_name);
+ fprintf(stderr, " sudo systemctl restart %s\n", service_name);
+ return 0;
+}
+
+static __attribute__((unused)) int maybe_prompt_write_path(char* path_out, size_t path_out_size) {
if (!path_out || path_out_size == 0) return -1;
char input[WIZARD_LINE_MAX];
if (read_line_prompt("Path [./genesis.jsonc]: ", input, sizeof(input)) != 0) return -1;
@@ -1298,7 +1565,7 @@ static int maybe_prompt_write_path(char* path_out, size_t path_out_size) {
static int new_agent_identity_step(didactyl_config_t* cfg) {
for (;;) {
- render_wizard_page_header("Step 1 of 7", "New Agent Setup -- Identity");
+ render_wizard_page_header("Step 2 of 7", "New Agent Setup -- Identity");
print_option('g', "enerate a new Nostr keypair");
print_option('p', "rovide an existing nsec");
print_option('b', "ack");
@@ -1359,6 +1626,16 @@ static int new_agent_identity_step(didactyl_config_t* cfg) {
}
static int new_agent_flow(didactyl_config_t* cfg, char* genesis_path_out, size_t genesis_path_out_size) {
+ (void)genesis_path_out;
+ (void)genesis_path_out_size;
+ char agent_name[OW_MAX_NAME_LEN] = {0};
+ render_wizard_page_header("Step 1 of 7", "New Agent Setup -- Agent Profile");
+ if (read_line_prompt(" Agent name [Didactyl]: ", agent_name, sizeof(agent_name)) != 0) return -1;
+ if (line_is_quit(agent_name)) return -1;
+ if (agent_name[0] == '\0') {
+ snprintf(agent_name, sizeof(agent_name), "%s", "Didactyl");
+ }
+
int rc = new_agent_identity_step(cfg);
if (rc != 0) return rc < 0 ? -1 : 1;
@@ -1372,7 +1649,7 @@ static int new_agent_flow(didactyl_config_t* cfg, char* genesis_path_out, size_t
ANSI_YELLOW,
ANSI_RESET);
} else {
- render_wizard_page_header("Step 3 of 7", "New Agent Setup -- Identity Validation");
+ render_wizard_page_header("Step 4 of 7", "New Agent Setup -- Identity Validation");
fprintf(stderr, " Agent kind 10002: %s\n", exists ? "FOUND (may overwrite existing state)" : "Agent npub available for new setup");
if (admin_name_found) {
fprintf(stderr, " Admin profile: %s\n", admin_name);
@@ -1400,21 +1677,13 @@ static int new_agent_flow(didactyl_config_t* cfg, char* genesis_path_out, size_t
if (prompt_llm_config(cfg) != 0) return -1;
}
- char agent_name[OW_MAX_NAME_LEN] = {0};
- render_wizard_page_header("Step 6 of 7", "New Agent Setup -- Agent Profile");
- if (read_line_prompt(" Agent name [Didactyl]: ", agent_name, sizeof(agent_name)) != 0) return -1;
- if (line_is_quit(agent_name)) return -1;
- if (agent_name[0] == '\0') {
- snprintf(agent_name, sizeof(agent_name), "%s", "Didactyl");
- }
-
- if (sync_startup_kind0_event(cfg, agent_name) != 0) {
- fprintf(stderr, "%sFailed to prepare startup kind0 profile event.%s\n", ANSI_RED, ANSI_RESET);
+ if (configure_default_skill_for_agent(cfg, agent_name) != 0) {
+ fprintf(stderr, "%sFailed to prepare default skill event content.%s\n", ANSI_RED, ANSI_RESET);
return -1;
}
- if (sync_startup_kind10002_event(cfg) != 0) {
- fprintf(stderr, "%sFailed to prepare startup kind10002 event.%s\n", ANSI_RED, ANSI_RESET);
+ if (apply_default_startup_events(cfg, agent_name) != 0) {
+ fprintf(stderr, "%sFailed to prepare startup events from defaults.%s\n", ANSI_RED, ANSI_RESET);
return -1;
}
@@ -1427,14 +1696,12 @@ static int new_agent_flow(didactyl_config_t* cfg, char* genesis_path_out, size_t
fprintf(stderr, " DM Protocol: nip04\n\n");
print_option('b', "oot the agent now");
- print_option('w', "rite genesis.jsonc (without nsec) and boot");
- print_option('i', "nclude nsec in genesis.jsonc and boot (security risk)");
- print_option('e', "xport genesis.jsonc (without nsec) and exit");
+ print_option('i', "nstall dedicated-user systemd service and boot");
print_option('s', "tart over");
print_option('q', "uit");
- wizard_option_t opts[] = {{'b', ""}, {'w', ""}, {'i', ""}, {'e', ""}, {'s', ""}, {'q', ""}};
- char c = read_menu_choice(opts, 6);
+ wizard_option_t opts[] = {{'b', ""}, {'i', ""}, {'s', ""}, {'q', ""}};
+ char c = read_menu_choice(opts, 4);
if (c == 'q') return -1;
if (c == 's') return 2;
@@ -1445,26 +1712,11 @@ static int new_agent_flow(didactyl_config_t* cfg, char* genesis_path_out, size_t
return 0;
}
- if (c == 'w' || c == 'i' || c == 'e') {
- char path[OW_MAX_URL_LEN] = {0};
- if (maybe_prompt_write_path(path, sizeof(path)) != 0) return -1;
- int include_nsec = (c == 'i') ? 1 : 0;
- if (include_nsec) {
- fprintf(stderr, "%sWarning: this writes nsec to disk in plaintext.%s\n", ANSI_YELLOW, ANSI_RESET);
- }
- if (write_genesis_file(cfg, path, include_nsec) != 0) {
- fprintf(stderr, "%sFailed to write genesis file: %s (%s)%s\n",
- ANSI_RED,
- path,
- strerror(errno),
- ANSI_RESET);
+ if (c == 'i') {
+ if (install_system_service_with_dedicated_user(cfg, agent_name) != 0) {
+ fprintf(stderr, "%sFailed to install dedicated-user system service.%s\n", ANSI_RED, ANSI_RESET);
return -1;
}
- fprintf(stderr, "Wrote %s\n", path);
- if (genesis_path_out && genesis_path_out_size > 0) {
- snprintf(genesis_path_out, genesis_path_out_size, "%s", path);
- }
- if (c == 'e') return 1;
fprintf(stderr, "%sStep 7 of 7 -- Booting new agent. Check your messages for initial greeting.%s\n",
ANSI_YELLOW,
ANSI_RESET);
diff --git a/src/tools/tool_nostr_my_events.c b/src/tools/tool_nostr_my_events.c
new file mode 100644
index 0000000..a131bcd
--- /dev/null
+++ b/src/tools/tool_nostr_my_events.c
@@ -0,0 +1,212 @@
+#define _POSIX_C_SOURCE 200809L
+
+#include "tools_internal.h"
+
+#include
+#include
+#include
+
+#include "cjson/cJSON.h"
+#include "../nostr_handler.h"
+
+static char* json_error_local(const char* msg) {
+ cJSON* root = cJSON_CreateObject();
+ if (!root) return NULL;
+ cJSON_AddBoolToObject(root, "success", 0);
+ cJSON_AddStringToObject(root, "error", msg ? msg : "unknown error");
+ char* out = cJSON_PrintUnformatted(root);
+ cJSON_Delete(root);
+ return out;
+}
+
+static const char* find_d_tag_local(cJSON* tags) {
+ if (!tags || !cJSON_IsArray(tags)) {
+ return NULL;
+ }
+
+ int n = cJSON_GetArraySize(tags);
+ for (int i = 0; i < n; i++) {
+ cJSON* tag = cJSON_GetArrayItem(tags, i);
+ if (!tag || !cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) {
+ continue;
+ }
+
+ cJSON* key = cJSON_GetArrayItem(tag, 0);
+ cJSON* value = cJSON_GetArrayItem(tag, 1);
+ if (!key || !value || !cJSON_IsString(key) || !cJSON_IsString(value) ||
+ !key->valuestring || !value->valuestring) {
+ continue;
+ }
+
+ if (strcmp(key->valuestring, "d") == 0) {
+ return value->valuestring;
+ }
+ }
+
+ return NULL;
+}
+
+char* execute_nostr_my_events(tools_context_t* ctx, const char* args_json) {
+ if (!ctx || !ctx->cfg) return json_error_local("tool context unavailable");
+
+ cJSON* args = cJSON_Parse(args_json ? args_json : "{}");
+ if (!args || !cJSON_IsObject(args)) {
+ cJSON_Delete(args);
+ return json_error_local("invalid arguments JSON");
+ }
+
+ int limit = 200;
+ int timeout_ms = 8000;
+ int kind_filter = -1;
+
+ cJSON* limit_item = cJSON_GetObjectItemCaseSensitive(args, "limit");
+ if (limit_item) {
+ if (!cJSON_IsNumber(limit_item)) {
+ cJSON_Delete(args);
+ return json_error_local("limit must be an integer");
+ }
+ limit = (int)limit_item->valuedouble;
+ if (limit < 1) limit = 1;
+ if (limit > 2000) limit = 2000;
+ }
+
+ cJSON* timeout_item = cJSON_GetObjectItemCaseSensitive(args, "timeout_ms");
+ if (timeout_item) {
+ if (!cJSON_IsNumber(timeout_item)) {
+ cJSON_Delete(args);
+ return json_error_local("timeout_ms must be an integer");
+ }
+ timeout_ms = (int)timeout_item->valuedouble;
+ if (timeout_ms < 1000) timeout_ms = 1000;
+ if (timeout_ms > 60000) timeout_ms = 60000;
+ }
+
+ cJSON* kind_item = cJSON_GetObjectItemCaseSensitive(args, "kind");
+ if (kind_item) {
+ if (!cJSON_IsNumber(kind_item)) {
+ cJSON_Delete(args);
+ return json_error_local("kind must be an integer when provided");
+ }
+ kind_filter = (int)kind_item->valuedouble;
+ if (kind_filter < 0) {
+ cJSON_Delete(args);
+ return json_error_local("kind must be >= 0");
+ }
+ }
+
+ cJSON* filter = cJSON_CreateObject();
+ cJSON* authors = cJSON_CreateArray();
+ if (!filter || !authors) {
+ cJSON_Delete(filter);
+ cJSON_Delete(authors);
+ cJSON_Delete(args);
+ return json_error_local("failed to build filter");
+ }
+
+ cJSON_AddItemToArray(authors, cJSON_CreateString(ctx->cfg->keys.public_key_hex));
+ cJSON_AddItemToObject(filter, "authors", authors);
+ if (kind_filter >= 0) {
+ cJSON* kinds = cJSON_CreateArray();
+ if (!kinds) {
+ cJSON_Delete(filter);
+ cJSON_Delete(args);
+ return json_error_local("failed to build kind filter");
+ }
+ cJSON_AddItemToArray(kinds, cJSON_CreateNumber(kind_filter));
+ cJSON_AddItemToObject(filter, "kinds", kinds);
+ }
+ cJSON_AddNumberToObject(filter, "limit", limit);
+
+ char* events_json = nostr_handler_query_json(filter, timeout_ms);
+ cJSON_Delete(filter);
+ cJSON_Delete(args);
+ if (!events_json) {
+ return json_error_local("nostr query failed");
+ }
+
+ cJSON* events = cJSON_Parse(events_json);
+ free(events_json);
+ if (!events || !cJSON_IsArray(events)) {
+ cJSON_Delete(events);
+ return json_error_local("nostr query returned invalid JSON");
+ }
+
+ cJSON* summarized = cJSON_CreateArray();
+ cJSON* seen_event_ids = cJSON_CreateObject();
+ if (!summarized || !seen_event_ids) {
+ cJSON_Delete(events);
+ cJSON_Delete(summarized);
+ cJSON_Delete(seen_event_ids);
+ return json_error_local("allocation failure");
+ }
+
+ 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* id = cJSON_GetObjectItemCaseSensitive(ev, "id");
+ cJSON* created_at = cJSON_GetObjectItemCaseSensitive(ev, "created_at");
+ cJSON* tags = cJSON_GetObjectItemCaseSensitive(ev, "tags");
+
+ if (!kind || !cJSON_IsNumber(kind) ||
+ !id || !cJSON_IsString(id) || !id->valuestring ||
+ !created_at || !cJSON_IsNumber(created_at)) {
+ continue;
+ }
+
+ cJSON* already_seen = cJSON_GetObjectItemCaseSensitive(seen_event_ids, id->valuestring);
+ if (already_seen) {
+ continue;
+ }
+ cJSON_AddBoolToObject(seen_event_ids, id->valuestring, 1);
+
+ cJSON* item = cJSON_CreateObject();
+ if (!item) {
+ continue;
+ }
+
+ cJSON_AddNumberToObject(item, "kind", (int)kind->valuedouble);
+ cJSON_AddStringToObject(item, "event_id", id->valuestring);
+ {
+ char ts_buf[32];
+ long long ts = (long long)created_at->valuedouble;
+ snprintf(ts_buf, sizeof(ts_buf), "%lld", ts);
+ cJSON_AddStringToObject(item, "timestamp", ts_buf);
+ }
+
+ const char* d_tag = find_d_tag_local(tags);
+ if (d_tag && d_tag[0] != '\0') {
+ cJSON_AddStringToObject(item, "d_tag", d_tag);
+ } else {
+ cJSON_AddNullToObject(item, "d_tag");
+ }
+
+ cJSON_AddBoolToObject(item,
+ "in_current_cache",
+ nostr_handler_is_event_in_self_cache(id->valuestring) ? 1 : 0);
+
+ cJSON_AddItemToArray(summarized, item);
+ }
+
+ cJSON* out = cJSON_CreateObject();
+ if (!out) {
+ cJSON_Delete(events);
+ cJSON_Delete(summarized);
+ cJSON_Delete(seen_event_ids);
+ return json_error_local("allocation failure");
+ }
+
+ cJSON_AddBoolToObject(out, "success", 1);
+ cJSON_AddNumberToObject(out, "count", cJSON_GetArraySize(summarized));
+ cJSON_AddItemToObject(out, "events", summarized);
+
+ char* json = cJSON_PrintUnformatted(out);
+ cJSON_Delete(out);
+ cJSON_Delete(events);
+ cJSON_Delete(seen_event_ids);
+ return json;
+}
diff --git a/src/tools/tool_nostr_relay.c b/src/tools/tool_nostr_relay.c
index 7ca34a3..cafd1cc 100644
--- a/src/tools/tool_nostr_relay.c
+++ b/src/tools/tool_nostr_relay.c
@@ -92,3 +92,105 @@ char* execute_nostr_relay_info(const char* args_json) {
cJSON_Delete(out);
return json;
}
+
+char* execute_nostr_subscription_status(const char* args_json) {
+ cJSON* args = parse_args_local(args_json);
+ if (!args) return json_error_local("invalid arguments JSON");
+ cJSON_Delete(args);
+
+ char* status_json = nostr_handler_subscription_status_json();
+ if (!status_json) {
+ return json_error_local("nostr_subscription_status failed");
+ }
+
+ cJSON* status = cJSON_Parse(status_json);
+ free(status_json);
+ if (!status || !cJSON_IsObject(status)) {
+ cJSON_Delete(status);
+ return json_error_local("nostr_subscription_status returned invalid JSON");
+ }
+
+ cJSON* out = cJSON_CreateObject();
+ if (!out) {
+ cJSON_Delete(status);
+ return NULL;
+ }
+
+ cJSON_AddBoolToObject(out, "success", 1);
+ cJSON_AddItemToObject(out, "status", status);
+
+ char* json = cJSON_PrintUnformatted(out);
+ cJSON_Delete(out);
+ return json;
+}
+
+char* execute_nostr_subscription_set(const char* args_json) {
+ cJSON* args = parse_args_local(args_json);
+ if (!args) return json_error_local("invalid arguments JSON");
+
+ cJSON* name = cJSON_GetObjectItemCaseSensitive(args, "name");
+ if (!name || !cJSON_IsString(name) || !name->valuestring || name->valuestring[0] == '\0') {
+ cJSON_Delete(args);
+ return json_error_local("nostr_subscription_set requires non-empty string name");
+ }
+
+ cJSON* enabled_item = cJSON_GetObjectItemCaseSensitive(args, "enabled");
+ int enabled_set = 0;
+ int enabled = 0;
+ if (enabled_item) {
+ if (!cJSON_IsBool(enabled_item)) {
+ cJSON_Delete(args);
+ return json_error_local("nostr_subscription_set enabled must be boolean");
+ }
+ enabled_set = 1;
+ enabled = cJSON_IsTrue(enabled_item) ? 1 : 0;
+ }
+
+ cJSON* filter_item = cJSON_GetObjectItemCaseSensitive(args, "filter");
+ cJSON* filter_dup = NULL;
+ if (filter_item) {
+ if (!cJSON_IsObject(filter_item)) {
+ cJSON_Delete(args);
+ return json_error_local("nostr_subscription_set filter must be an object");
+ }
+ filter_dup = cJSON_Duplicate(filter_item, 1);
+ if (!filter_dup) {
+ cJSON_Delete(args);
+ return json_error_local("nostr_subscription_set filter copy failed");
+ }
+ }
+
+ int rc = nostr_handler_subscription_set_json(name->valuestring, filter_dup, enabled, enabled_set);
+ cJSON_Delete(filter_dup);
+
+ char* status_json = nostr_handler_subscription_status_json();
+ cJSON_Delete(args);
+
+ if (rc != 0) {
+ free(status_json);
+ return json_error_local("nostr_subscription_set failed");
+ }
+
+ cJSON* status = status_json ? cJSON_Parse(status_json) : NULL;
+ free(status_json);
+
+ cJSON* out = cJSON_CreateObject();
+ if (!out) {
+ cJSON_Delete(status);
+ return NULL;
+ }
+
+ cJSON_AddBoolToObject(out, "success", 1);
+ cJSON_AddStringToObject(out, "name", name->valuestring);
+ cJSON_AddBoolToObject(out, "updated", 1);
+ if (enabled_set) {
+ cJSON_AddBoolToObject(out, "enabled", enabled ? 1 : 0);
+ }
+ if (status) {
+ cJSON_AddItemToObject(out, "status", status);
+ }
+
+ char* json = cJSON_PrintUnformatted(out);
+ cJSON_Delete(out);
+ return json;
+}
diff --git a/src/tools/tools_dispatch.c b/src/tools/tools_dispatch.c
index 989ef39..1664445 100644
--- a/src/tools/tools_dispatch.c
+++ b/src/tools/tools_dispatch.c
@@ -66,6 +66,12 @@ char* tools_execute_legacy(tools_context_t* ctx, const char* tool_name, const ch
if (strcmp(tool_name, "nostr_relay_info") == 0) {
return execute_nostr_relay_info(args_json);
}
+ if (strcmp(tool_name, "nostr_subscription_status") == 0) {
+ return execute_nostr_subscription_status(args_json);
+ }
+ if (strcmp(tool_name, "nostr_subscription_set") == 0) {
+ return execute_nostr_subscription_set(args_json);
+ }
if (strcmp(tool_name, "nostr_encrypt") == 0) {
return execute_nostr_encrypt(ctx, args_json);
}
@@ -81,6 +87,9 @@ char* tools_execute_legacy(tools_context_t* ctx, const char* tool_name, const ch
if (strcmp(tool_name, "nostr_query") == 0) {
return execute_nostr_query(args_json);
}
+ if (strcmp(tool_name, "nostr_my_events") == 0) {
+ return execute_nostr_my_events(ctx, args_json);
+ }
if (strcmp(tool_name, "agent_version") == 0) {
return execute_agent_version(args_json);
}
diff --git a/src/tools/tools_internal.h b/src/tools/tools_internal.h
index 5b19fcc..3f51306 100644
--- a/src/tools/tools_internal.h
+++ b/src/tools/tools_internal.h
@@ -21,6 +21,7 @@ char* execute_model_get(const char* args_json);
char* execute_model_set(tools_context_t* ctx, const char* args_json);
char* execute_model_list(const char* args_json);
char* execute_nostr_query(const char* args_json);
+char* execute_nostr_my_events(tools_context_t* ctx, const char* args_json);
char* execute_nostr_pubkey(tools_context_t* ctx, const char* args_json);
char* execute_nostr_npub(tools_context_t* ctx, const char* args_json);
char* execute_nostr_encode(const char* args_json);
@@ -30,6 +31,8 @@ char* execute_nostr_profile_get(const char* args_json);
char* execute_nostr_nip05_lookup(const char* args_json);
char* execute_nostr_relay_status(const char* args_json);
char* execute_nostr_relay_info(const char* args_json);
+char* execute_nostr_subscription_status(const char* args_json);
+char* execute_nostr_subscription_set(const char* args_json);
char* execute_nostr_dm_send(const char* args_json);
char* execute_nostr_encrypt(tools_context_t* ctx, const char* args_json);
char* execute_nostr_decrypt(tools_context_t* ctx, const char* args_json);
diff --git a/src/tools/tools_schema.c b/src/tools/tools_schema.c
index a449281..2e2bdd0 100644
--- a/src/tools/tools_schema.c
+++ b/src/tools/tools_schema.c
@@ -410,6 +410,54 @@ char* tools_build_openai_schema_json_legacy(const tools_context_t* ctx) {
cJSON_AddItemToObject(t15, "function", t15_fn);
cJSON_AddItemToArray(tools, t15);
+ cJSON* t15b = cJSON_CreateObject();
+ cJSON* t15b_fn = cJSON_CreateObject();
+ cJSON* t15b_params = cJSON_CreateObject();
+ cJSON* t15b_props = cJSON_CreateObject();
+
+ cJSON_AddStringToObject(t15b, "type", "function");
+ cJSON_AddStringToObject(t15b_fn, "name", "nostr_subscription_status");
+ cJSON_AddStringToObject(t15b_fn, "description", "List currently managed runtime Nostr subscriptions and filters");
+ cJSON_AddStringToObject(t15b_params, "type", "object");
+ cJSON_AddItemToObject(t15b_params, "properties", t15b_props);
+ cJSON_AddItemToObject(t15b_fn, "parameters", t15b_params);
+ cJSON_AddItemToObject(t15b, "function", t15b_fn);
+ cJSON_AddItemToArray(tools, t15b);
+
+ cJSON* t15c = cJSON_CreateObject();
+ cJSON* t15c_fn = cJSON_CreateObject();
+ cJSON* t15c_params = cJSON_CreateObject();
+ cJSON* t15c_props = cJSON_CreateObject();
+ cJSON* t15c_required = cJSON_CreateArray();
+
+ cJSON_AddStringToObject(t15c, "type", "function");
+ cJSON_AddStringToObject(t15c_fn, "name", "nostr_subscription_set");
+ cJSON_AddStringToObject(t15c_fn, "description", "Update one managed runtime subscription by name (toggle enabled and/or replace filter)");
+ cJSON_AddStringToObject(t15c_params, "type", "object");
+ cJSON_AddItemToObject(t15c_params, "properties", t15c_props);
+ cJSON_AddItemToObject(t15c_params, "required", t15c_required);
+
+ cJSON* p_sub_name = cJSON_CreateObject();
+ cJSON_AddStringToObject(p_sub_name, "type", "string");
+ cJSON_AddStringToObject(p_sub_name, "description", "Subscription name (e.g. admin_context_profile, agent_context_notes, dms_kind4)");
+ cJSON_AddItemToObject(t15c_props, "name", p_sub_name);
+
+ cJSON* p_sub_enabled = cJSON_CreateObject();
+ cJSON_AddStringToObject(p_sub_enabled, "type", "boolean");
+ cJSON_AddStringToObject(p_sub_enabled, "description", "Optional: enable or disable this subscription");
+ cJSON_AddItemToObject(t15c_props, "enabled", p_sub_enabled);
+
+ cJSON* p_sub_filter = cJSON_CreateObject();
+ cJSON_AddStringToObject(p_sub_filter, "type", "object");
+ cJSON_AddStringToObject(p_sub_filter, "description", "Optional: replacement Nostr filter object");
+ cJSON_AddItemToObject(t15c_props, "filter", p_sub_filter);
+
+ cJSON_AddItemToArray(t15c_required, cJSON_CreateString("name"));
+
+ cJSON_AddItemToObject(t15c_fn, "parameters", t15c_params);
+ cJSON_AddItemToObject(t15c, "function", t15c_fn);
+ cJSON_AddItemToArray(tools, t15c);
+
cJSON* t16 = cJSON_CreateObject();
cJSON* t16_fn = cJSON_CreateObject();
cJSON* t16_params = cJSON_CreateObject();
@@ -1393,6 +1441,36 @@ char* tools_build_openai_schema_json_legacy(const tools_context_t* ctx) {
cJSON_AddItemToObject(t49, "function", t49_fn);
cJSON_AddItemToArray(tools, t49);
+ cJSON* t50 = cJSON_CreateObject();
+ cJSON* t50_fn = cJSON_CreateObject();
+ cJSON* t50_params = cJSON_CreateObject();
+ cJSON* t50_props = cJSON_CreateObject();
+
+ cJSON_AddStringToObject(t50, "type", "function");
+ cJSON_AddStringToObject(t50_fn, "name", "nostr_my_events");
+ cJSON_AddStringToObject(t50_fn, "description", "Query recent events authored by this agent and return kind, event_id, timestamp, d_tag, and cache presence");
+ cJSON_AddStringToObject(t50_params, "type", "object");
+ cJSON_AddItemToObject(t50_params, "properties", t50_props);
+
+ cJSON* p_my_events_limit = cJSON_CreateObject();
+ cJSON_AddStringToObject(p_my_events_limit, "type", "integer");
+ cJSON_AddStringToObject(p_my_events_limit, "description", "Maximum number of authored events to query (1-2000, default 200)");
+ cJSON_AddItemToObject(t50_props, "limit", p_my_events_limit);
+
+ cJSON* p_my_events_kind = cJSON_CreateObject();
+ cJSON_AddStringToObject(p_my_events_kind, "type", "integer");
+ cJSON_AddStringToObject(p_my_events_kind, "description", "Optional specific kind to query; omit for all kinds");
+ cJSON_AddItemToObject(t50_props, "kind", p_my_events_kind);
+
+ cJSON* p_my_events_timeout = cJSON_CreateObject();
+ cJSON_AddStringToObject(p_my_events_timeout, "type", "integer");
+ cJSON_AddStringToObject(p_my_events_timeout, "description", "Query timeout in milliseconds (1000-60000, default 8000)");
+ cJSON_AddItemToObject(t50_props, "timeout_ms", p_my_events_timeout);
+
+ cJSON_AddItemToObject(t50_fn, "parameters", t50_params);
+ cJSON_AddItemToObject(t50, "function", t50_fn);
+ cJSON_AddItemToArray(tools, t50);
+
char* out = cJSON_PrintUnformatted(tools);
cJSON_Delete(tools);
return out;