v0.0.73 - Refined Nostr handler and tool dispatch/schema updates for relay and my-events flows
This commit is contained in:
@@ -97,7 +97,7 @@ RUN if [ "$DEBUG_BUILD" = "true" ]; then \
|
|||||||
src/main.c src/config.c src/context.c src/llm.c \
|
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/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_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_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_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 \
|
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 \
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ SRCS = \
|
|||||||
$(SRC_DIR)/tools/tool_meta.c \
|
$(SRC_DIR)/tools/tool_meta.c \
|
||||||
$(SRC_DIR)/tools/tool_model.c \
|
$(SRC_DIR)/tools/tool_model.c \
|
||||||
$(SRC_DIR)/tools/tool_nostr_query.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_identity.c \
|
||||||
$(SRC_DIR)/tools/tool_nostr_social.c \
|
$(SRC_DIR)/tools/tool_nostr_social.c \
|
||||||
$(SRC_DIR)/tools/tool_nostr_relay.c \
|
$(SRC_DIR)/tools/tool_nostr_relay.c \
|
||||||
|
|||||||
@@ -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.
|
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.**
|
**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
|
- Connects to configured relays with auto-reconnect and relay state transition logging
|
||||||
- Publishes configured startup events per relay as each relay becomes connected
|
- Publishes configured startup events per relay as each relay becomes connected
|
||||||
|
|||||||
@@ -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_profile source="nostr_kind_0">
|
||||||
|
{{admin_kind0_json}}
|
||||||
|
</admin_kind0_profile>
|
||||||
|
|
||||||
|
- 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}}
|
||||||
@@ -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<br/>wss://relay.damus.io<br/>wss://relay.primal.net]
|
||||||
|
LLM_API[LLM Provider API<br/>OpenAI / PPQ / Ollama]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph Server
|
||||||
|
subgraph "systemd"
|
||||||
|
SVC[didactyl.service<br/>User=didactyl<br/>Group=didactyl]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph "/home/didactyl/"
|
||||||
|
BIN[didactyl binary]
|
||||||
|
CFG[genesis.jsonc<br/>mode 600]
|
||||||
|
LOGS[context.log.md<br/>didactyl.log]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph "Privilege Boundary"
|
||||||
|
SUDOERS[/etc/sudoers.d/didactyl<br/>Whitelisted commands only]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph "Unprivileged Commands"
|
||||||
|
MON[free / df / uptime<br/>ps / cat /proc/*<br/>ss -tlnp / top]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph "Privileged Commands via sudo"
|
||||||
|
SYSD[systemctl status/restart/start/stop<br/>journalctl<br/>daemon-reload]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
ADMIN[Administrator<br/>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
|
||||||
|
```
|
||||||
+71
-4
@@ -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* skill_root = skill_list_json ? cJSON_Parse(skill_list_json) : NULL;
|
||||||
cJSON* skills = skill_root ? cJSON_GetObjectItemCaseSensitive(skill_root, "skills") : NULL;
|
cJSON* skills = skill_root ? cJSON_GetObjectItemCaseSensitive(skill_root, "skills") : NULL;
|
||||||
|
|
||||||
if (!skills || !cJSON_IsArray(skills) || cJSON_GetArraySize(skills) <= 0) {
|
int available_count = 0;
|
||||||
(void)append_textf_local(&out, &cap, &used, "- (none)\n");
|
if (skills && cJSON_IsArray(skills) && cJSON_GetArraySize(skills) > 0) {
|
||||||
} else {
|
|
||||||
int sn = cJSON_GetArraySize(skills);
|
int sn = cJSON_GetArraySize(skills);
|
||||||
for (int i = 0; i < sn; i++) {
|
for (int i = 0; i < sn; i++) {
|
||||||
cJSON* row = cJSON_GetArrayItem(skills, 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* d_tag = row ? cJSON_GetObjectItemCaseSensitive(row, "d_tag") : NULL;
|
||||||
cJSON* desc = row ? cJSON_GetObjectItemCaseSensitive(row, "description") : 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* 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 : "";
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (append_textf_local(&out, &cap, &used, "- %s%s%s\n", d_tag_s, desc_s[0] ? " — " : "", desc_s) != 0) {
|
if (append_textf_local(&out, &cap, &used, "- %s%s%s\n", d_tag_s, desc_s[0] ? " — " : "", desc_s) != 0) {
|
||||||
cJSON_Delete(skill_root);
|
cJSON_Delete(skill_root);
|
||||||
free(skill_list_json);
|
free(skill_list_json);
|
||||||
free(out);
|
free(out);
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
available_count++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (available_count <= 0) {
|
||||||
|
(void)append_textf_local(&out, &cap, &used, "- (none)\n");
|
||||||
|
}
|
||||||
|
|
||||||
cJSON_Delete(skill_root);
|
cJSON_Delete(skill_root);
|
||||||
free(skill_list_json);
|
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;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
+71
-4
@@ -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* skill_root = skill_list_json ? cJSON_Parse(skill_list_json) : NULL;
|
||||||
cJSON* skills = skill_root ? cJSON_GetObjectItemCaseSensitive(skill_root, "skills") : NULL;
|
cJSON* skills = skill_root ? cJSON_GetObjectItemCaseSensitive(skill_root, "skills") : NULL;
|
||||||
|
|
||||||
if (!skills || !cJSON_IsArray(skills) || cJSON_GetArraySize(skills) <= 0) {
|
int available_count = 0;
|
||||||
(void)append_textf_http(&out, &cap, &used, "- (none)\n");
|
if (skills && cJSON_IsArray(skills) && cJSON_GetArraySize(skills) > 0) {
|
||||||
} else {
|
|
||||||
int sn = cJSON_GetArraySize(skills);
|
int sn = cJSON_GetArraySize(skills);
|
||||||
for (int i = 0; i < sn; i++) {
|
for (int i = 0; i < sn; i++) {
|
||||||
cJSON* row = cJSON_GetArrayItem(skills, 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* d_tag = row ? cJSON_GetObjectItemCaseSensitive(row, "d_tag") : NULL;
|
||||||
cJSON* desc = row ? cJSON_GetObjectItemCaseSensitive(row, "description") : 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* 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 : "";
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (append_textf_http(&out, &cap, &used, "- %s%s%s\n", d_tag_s, desc_s[0] ? " — " : "", desc_s) != 0) {
|
if (append_textf_http(&out, &cap, &used, "- %s%s%s\n", d_tag_s, desc_s[0] ? " — " : "", desc_s) != 0) {
|
||||||
cJSON_Delete(skill_root);
|
cJSON_Delete(skill_root);
|
||||||
free(skill_list_json);
|
free(skill_list_json);
|
||||||
free(out);
|
free(out);
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
available_count++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (available_count <= 0) {
|
||||||
|
(void)append_textf_http(&out, &cap, &used, "- (none)\n");
|
||||||
|
}
|
||||||
|
|
||||||
cJSON_Delete(skill_root);
|
cJSON_Delete(skill_root);
|
||||||
free(skill_list_json);
|
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;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+7
-1
@@ -1166,8 +1166,14 @@ int main(int argc, char** argv) {
|
|||||||
startup_step_ok(7, "Reconcile/persist startup state", NULL);
|
startup_step_ok(7, "Reconcile/persist startup state", NULL);
|
||||||
|
|
||||||
const char* system_context = nostr_handler_get_system_context();
|
const char* system_context = nostr_handler_get_system_context();
|
||||||
|
char system_context_fallback[512] = {0};
|
||||||
if (!system_context || system_context[0] == '\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");
|
startup_step_begin(8, "Initialize agent");
|
||||||
|
|||||||
+2
-2
@@ -12,8 +12,8 @@
|
|||||||
// Using DIDACTYL_ prefix to avoid conflicts with nostr_core_lib VERSION macros
|
// Using DIDACTYL_ prefix to avoid conflicts with nostr_core_lib VERSION macros
|
||||||
#define DIDACTYL_VERSION_MAJOR 0
|
#define DIDACTYL_VERSION_MAJOR 0
|
||||||
#define DIDACTYL_VERSION_MINOR 0
|
#define DIDACTYL_VERSION_MINOR 0
|
||||||
#define DIDACTYL_VERSION_PATCH 72
|
#define DIDACTYL_VERSION_PATCH 73
|
||||||
#define DIDACTYL_VERSION "v0.0.72"
|
#define DIDACTYL_VERSION "v0.0.73"
|
||||||
|
|
||||||
// Agent metadata
|
// Agent metadata
|
||||||
#define DIDACTYL_NAME "Didactyl"
|
#define DIDACTYL_NAME "Didactyl"
|
||||||
|
|||||||
+657
-120
@@ -51,6 +51,37 @@ static int g_agent_kind1_note_count = 0;
|
|||||||
|
|
||||||
static pthread_mutex_t g_admin_ctx_mutex = PTHREAD_MUTEX_INITIALIZER;
|
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 cJSON* g_self_skill_events = NULL;
|
||||||
static pthread_mutex_t g_self_skill_mutex = PTHREAD_MUTEX_INITIALIZER;
|
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 on_self_skill_event(cJSON* event, const char* relay_url, void* user_data);
|
||||||
static void dm_history_clear_locked(void);
|
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 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) {
|
static void log_relay_state_changes(void) {
|
||||||
if (!g_pool || !g_cfg || !g_last_relay_statuses) {
|
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) {
|
static void on_agent_context_event(cJSON* event, const char* relay_url, void* user_data) {
|
||||||
(void)relay_url;
|
|
||||||
(void)user_data;
|
(void)user_data;
|
||||||
|
|
||||||
if (!event || !g_cfg) {
|
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 k = (int)kind->valuedouble;
|
||||||
|
int runtime_sync_needed = 0;
|
||||||
|
|
||||||
pthread_mutex_lock(&g_admin_ctx_mutex);
|
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);
|
(void)parse_kind3_agent_contacts(tags);
|
||||||
} else if (k == 10002 && tags && cJSON_IsArray(tags)) {
|
} else if (k == 10002 && tags && cJSON_IsArray(tags)) {
|
||||||
(void)parse_kind10002_agent_relays(tags);
|
(void)parse_kind10002_agent_relays(tags);
|
||||||
|
runtime_sync_needed = 1;
|
||||||
} else if (k == 1 && content && cJSON_IsString(content) && content->valuestring) {
|
} 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);
|
time_t ts = (created_at && cJSON_IsNumber(created_at)) ? (time_t)created_at->valuedouble : time(NULL);
|
||||||
upsert_agent_kind1_note(ts, content->valuestring);
|
upsert_agent_kind1_note(ts, content->valuestring);
|
||||||
}
|
}
|
||||||
|
|
||||||
pthread_mutex_unlock(&g_admin_ctx_mutex);
|
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) {
|
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();
|
dm_history_clear_locked();
|
||||||
pthread_mutex_unlock(&g_dm_history_mutex);
|
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);
|
DEBUG_INFO("[didactyl] initializing relay pool with %d relays", g_cfg->relay_count);
|
||||||
|
|
||||||
nostr_core_set_log_bridge_if_supported();
|
nostr_core_set_log_bridge_if_supported();
|
||||||
@@ -1774,17 +2196,21 @@ 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_3) cJSON_AddItemToArray(profile_kinds, cJSON_CreateNumber(3));
|
||||||
if (g_cfg->admin_context.track_kind_10002) cJSON_AddItemToArray(profile_kinds, cJSON_CreateNumber(10002));
|
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_AddItemToObject(profile_filter, "kinds", profile_kinds);
|
||||||
cJSON_AddItemToArray(profile_authors, cJSON_CreateString(g_cfg->admin.pubkey));
|
cJSON_AddItemToArray(profile_authors, cJSON_CreateString(g_cfg->admin.pubkey));
|
||||||
cJSON_AddItemToObject(profile_filter, "authors", profile_authors);
|
cJSON_AddItemToObject(profile_filter, "authors", profile_authors);
|
||||||
cJSON_AddNumberToObject(profile_filter, "limit", 32);
|
cJSON_AddNumberToObject(profile_filter, "limit", 32);
|
||||||
|
} else {
|
||||||
|
cJSON_Delete(profile_kinds);
|
||||||
|
cJSON_Delete(profile_authors);
|
||||||
|
}
|
||||||
|
|
||||||
nostr_pool_subscription_t* profile_sub = nostr_relay_pool_subscribe(
|
pthread_mutex_lock(&g_subscription_mutex);
|
||||||
g_pool,
|
managed_subscriptions_init_defaults();
|
||||||
(const char**)g_cfg->relays,
|
if (managed_subscription_register_locked(MANAGED_SUB_ADMIN_PROFILE,
|
||||||
g_cfg->relay_count,
|
profile_enabled ? profile_filter : NULL,
|
||||||
profile_filter,
|
|
||||||
on_admin_context_event,
|
on_admin_context_event,
|
||||||
on_eose,
|
on_eose,
|
||||||
NULL,
|
NULL,
|
||||||
@@ -1792,19 +2218,17 @@ int nostr_handler_subscribe_admin_context(void) {
|
|||||||
1,
|
1,
|
||||||
NOSTR_POOL_EOSE_FULL_SET,
|
NOSTR_POOL_EOSE_FULL_SET,
|
||||||
30,
|
30,
|
||||||
120);
|
120,
|
||||||
|
profile_enabled) != 0) {
|
||||||
if (!profile_sub) {
|
|
||||||
rc = -1;
|
rc = -1;
|
||||||
}
|
}
|
||||||
} else {
|
pthread_mutex_unlock(&g_subscription_mutex);
|
||||||
cJSON_Delete(profile_kinds);
|
|
||||||
cJSON_Delete(profile_authors);
|
|
||||||
}
|
|
||||||
cJSON_Delete(profile_filter);
|
cJSON_Delete(profile_filter);
|
||||||
|
|
||||||
if (g_cfg->admin_context.track_kind_1) {
|
int notes_enabled = g_cfg->admin_context.track_kind_1 ? 1 : 0;
|
||||||
cJSON* notes_filter = cJSON_CreateObject();
|
cJSON* notes_filter = NULL;
|
||||||
|
if (notes_enabled) {
|
||||||
|
notes_filter = cJSON_CreateObject();
|
||||||
cJSON* notes_kinds = cJSON_CreateArray();
|
cJSON* notes_kinds = cJSON_CreateArray();
|
||||||
cJSON* notes_authors = cJSON_CreateArray();
|
cJSON* notes_authors = cJSON_CreateArray();
|
||||||
if (!notes_filter || !notes_kinds || !notes_authors) {
|
if (!notes_filter || !notes_kinds || !notes_authors) {
|
||||||
@@ -1824,11 +2248,11 @@ int nostr_handler_subscribe_admin_context(void) {
|
|||||||
cJSON_AddItemToArray(notes_authors, cJSON_CreateString(g_cfg->admin.pubkey));
|
cJSON_AddItemToArray(notes_authors, cJSON_CreateString(g_cfg->admin.pubkey));
|
||||||
cJSON_AddItemToObject(notes_filter, "authors", notes_authors);
|
cJSON_AddItemToObject(notes_filter, "authors", notes_authors);
|
||||||
cJSON_AddNumberToObject(notes_filter, "limit", kind1_limit);
|
cJSON_AddNumberToObject(notes_filter, "limit", kind1_limit);
|
||||||
|
}
|
||||||
|
|
||||||
nostr_pool_subscription_t* notes_sub = nostr_relay_pool_subscribe(
|
pthread_mutex_lock(&g_subscription_mutex);
|
||||||
g_pool,
|
managed_subscriptions_init_defaults();
|
||||||
(const char**)g_cfg->relays,
|
if (managed_subscription_register_locked(MANAGED_SUB_ADMIN_NOTES,
|
||||||
g_cfg->relay_count,
|
|
||||||
notes_filter,
|
notes_filter,
|
||||||
on_admin_context_event,
|
on_admin_context_event,
|
||||||
on_eose,
|
on_eose,
|
||||||
@@ -1837,13 +2261,12 @@ int nostr_handler_subscribe_admin_context(void) {
|
|||||||
1,
|
1,
|
||||||
NOSTR_POOL_EOSE_FULL_SET,
|
NOSTR_POOL_EOSE_FULL_SET,
|
||||||
30,
|
30,
|
||||||
120);
|
120,
|
||||||
|
notes_enabled) != 0) {
|
||||||
cJSON_Delete(notes_filter);
|
|
||||||
if (!notes_sub) {
|
|
||||||
rc = -1;
|
rc = -1;
|
||||||
}
|
}
|
||||||
}
|
pthread_mutex_unlock(&g_subscription_mutex);
|
||||||
|
cJSON_Delete(notes_filter);
|
||||||
|
|
||||||
if (rc == 0) {
|
if (rc == 0) {
|
||||||
DEBUG_INFO("[didactyl] admin context subscriptions active for admin %.16s...", g_cfg->admin.pubkey);
|
DEBUG_INFO("[didactyl] admin context subscriptions active for admin %.16s...", g_cfg->admin.pubkey);
|
||||||
@@ -1876,10 +2299,9 @@ int nostr_handler_subscribe_agent_context(void) {
|
|||||||
cJSON_AddItemToObject(profile_filter, "authors", profile_authors);
|
cJSON_AddItemToObject(profile_filter, "authors", profile_authors);
|
||||||
cJSON_AddNumberToObject(profile_filter, "limit", 64);
|
cJSON_AddNumberToObject(profile_filter, "limit", 64);
|
||||||
|
|
||||||
nostr_pool_subscription_t* profile_sub = nostr_relay_pool_subscribe(
|
pthread_mutex_lock(&g_subscription_mutex);
|
||||||
g_pool,
|
managed_subscriptions_init_defaults();
|
||||||
(const char**)g_cfg->relays,
|
if (managed_subscription_register_locked(MANAGED_SUB_AGENT_PROFILE,
|
||||||
g_cfg->relay_count,
|
|
||||||
profile_filter,
|
profile_filter,
|
||||||
on_agent_context_event,
|
on_agent_context_event,
|
||||||
on_eose,
|
on_eose,
|
||||||
@@ -1888,12 +2310,12 @@ int nostr_handler_subscribe_agent_context(void) {
|
|||||||
1,
|
1,
|
||||||
NOSTR_POOL_EOSE_FULL_SET,
|
NOSTR_POOL_EOSE_FULL_SET,
|
||||||
30,
|
30,
|
||||||
120);
|
120,
|
||||||
|
1) != 0) {
|
||||||
cJSON_Delete(profile_filter);
|
|
||||||
if (!profile_sub) {
|
|
||||||
rc = -1;
|
rc = -1;
|
||||||
}
|
}
|
||||||
|
pthread_mutex_unlock(&g_subscription_mutex);
|
||||||
|
cJSON_Delete(profile_filter);
|
||||||
|
|
||||||
cJSON* notes_filter = cJSON_CreateObject();
|
cJSON* notes_filter = cJSON_CreateObject();
|
||||||
cJSON* notes_kinds = cJSON_CreateArray();
|
cJSON* notes_kinds = cJSON_CreateArray();
|
||||||
@@ -1916,10 +2338,9 @@ int nostr_handler_subscribe_agent_context(void) {
|
|||||||
cJSON_AddItemToObject(notes_filter, "authors", notes_authors);
|
cJSON_AddItemToObject(notes_filter, "authors", notes_authors);
|
||||||
cJSON_AddNumberToObject(notes_filter, "limit", kind1_limit);
|
cJSON_AddNumberToObject(notes_filter, "limit", kind1_limit);
|
||||||
|
|
||||||
nostr_pool_subscription_t* notes_sub = nostr_relay_pool_subscribe(
|
pthread_mutex_lock(&g_subscription_mutex);
|
||||||
g_pool,
|
managed_subscriptions_init_defaults();
|
||||||
(const char**)g_cfg->relays,
|
if (managed_subscription_register_locked(MANAGED_SUB_AGENT_NOTES,
|
||||||
g_cfg->relay_count,
|
|
||||||
notes_filter,
|
notes_filter,
|
||||||
on_agent_context_event,
|
on_agent_context_event,
|
||||||
on_eose,
|
on_eose,
|
||||||
@@ -1928,12 +2349,12 @@ int nostr_handler_subscribe_agent_context(void) {
|
|||||||
1,
|
1,
|
||||||
NOSTR_POOL_EOSE_FULL_SET,
|
NOSTR_POOL_EOSE_FULL_SET,
|
||||||
30,
|
30,
|
||||||
120);
|
120,
|
||||||
|
1) != 0) {
|
||||||
cJSON_Delete(notes_filter);
|
|
||||||
if (!notes_sub) {
|
|
||||||
rc = -1;
|
rc = -1;
|
||||||
}
|
}
|
||||||
|
pthread_mutex_unlock(&g_subscription_mutex);
|
||||||
|
cJSON_Delete(notes_filter);
|
||||||
|
|
||||||
if (rc == 0) {
|
if (rc == 0) {
|
||||||
DEBUG_INFO("[didactyl] agent self-context subscriptions active for pubkey %.16s...", g_cfg->keys.public_key_hex);
|
DEBUG_INFO("[didactyl] agent self-context subscriptions active for pubkey %.16s...", g_cfg->keys.public_key_hex);
|
||||||
@@ -1970,10 +2391,9 @@ int nostr_handler_subscribe_self_skills(void) {
|
|||||||
cJSON_AddItemToObject(filter, "authors", authors);
|
cJSON_AddItemToObject(filter, "authors", authors);
|
||||||
cJSON_AddNumberToObject(filter, "limit", 300);
|
cJSON_AddNumberToObject(filter, "limit", 300);
|
||||||
|
|
||||||
nostr_pool_subscription_t* skill_sub = nostr_relay_pool_subscribe(
|
pthread_mutex_lock(&g_subscription_mutex);
|
||||||
g_pool,
|
managed_subscriptions_init_defaults();
|
||||||
(const char**)g_cfg->relays,
|
if (managed_subscription_register_locked(MANAGED_SUB_SELF_SKILLS,
|
||||||
g_cfg->relay_count,
|
|
||||||
filter,
|
filter,
|
||||||
on_self_skill_event,
|
on_self_skill_event,
|
||||||
on_self_skill_eose,
|
on_self_skill_eose,
|
||||||
@@ -1982,15 +2402,17 @@ int nostr_handler_subscribe_self_skills(void) {
|
|||||||
0,
|
0,
|
||||||
NOSTR_POOL_EOSE_FULL_SET,
|
NOSTR_POOL_EOSE_FULL_SET,
|
||||||
30,
|
30,
|
||||||
120);
|
120,
|
||||||
|
1) != 0) {
|
||||||
cJSON_Delete(filter);
|
|
||||||
if (!skill_sub) {
|
|
||||||
rc = -1;
|
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) {
|
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 = cJSON_CreateObject();
|
cJSON* admin_filter = NULL;
|
||||||
|
if (admin_enabled) {
|
||||||
|
admin_filter = cJSON_CreateObject();
|
||||||
cJSON* admin_kinds = cJSON_CreateArray();
|
cJSON* admin_kinds = cJSON_CreateArray();
|
||||||
cJSON* admin_authors = cJSON_CreateArray();
|
cJSON* admin_authors = cJSON_CreateArray();
|
||||||
if (!admin_filter || !admin_kinds || !admin_authors) {
|
if (!admin_filter || !admin_kinds || !admin_authors) {
|
||||||
@@ -2005,11 +2427,11 @@ int nostr_handler_subscribe_self_skills(void) {
|
|||||||
cJSON_AddItemToArray(admin_authors, cJSON_CreateString(g_cfg->admin.pubkey));
|
cJSON_AddItemToArray(admin_authors, cJSON_CreateString(g_cfg->admin.pubkey));
|
||||||
cJSON_AddItemToObject(admin_filter, "authors", admin_authors);
|
cJSON_AddItemToObject(admin_filter, "authors", admin_authors);
|
||||||
cJSON_AddNumberToObject(admin_filter, "limit", 150);
|
cJSON_AddNumberToObject(admin_filter, "limit", 150);
|
||||||
|
}
|
||||||
|
|
||||||
nostr_pool_subscription_t* admin_skill_sub = nostr_relay_pool_subscribe(
|
pthread_mutex_lock(&g_subscription_mutex);
|
||||||
g_pool,
|
managed_subscriptions_init_defaults();
|
||||||
(const char**)g_cfg->relays,
|
if (managed_subscription_register_locked(MANAGED_SUB_ADMIN_SKILLS,
|
||||||
g_cfg->relay_count,
|
|
||||||
admin_filter,
|
admin_filter,
|
||||||
on_self_skill_event,
|
on_self_skill_event,
|
||||||
on_eose,
|
on_eose,
|
||||||
@@ -2018,13 +2440,12 @@ int nostr_handler_subscribe_self_skills(void) {
|
|||||||
0,
|
0,
|
||||||
NOSTR_POOL_EOSE_FULL_SET,
|
NOSTR_POOL_EOSE_FULL_SET,
|
||||||
30,
|
30,
|
||||||
120);
|
120,
|
||||||
|
admin_enabled) != 0) {
|
||||||
cJSON_Delete(admin_filter);
|
|
||||||
if (!admin_skill_sub) {
|
|
||||||
rc = -1;
|
rc = -1;
|
||||||
}
|
}
|
||||||
}
|
pthread_mutex_unlock(&g_subscription_mutex);
|
||||||
|
cJSON_Delete(admin_filter);
|
||||||
|
|
||||||
if (rc == 0) {
|
if (rc == 0) {
|
||||||
DEBUG_INFO("[didactyl] self/admin skill subscriptions active (self=%.16s..., admin=%.16s...)",
|
DEBUG_INFO("[didactyl] self/admin skill subscriptions active (self=%.16s..., admin=%.16s...)",
|
||||||
@@ -2050,12 +2471,16 @@ int nostr_handler_subscribe_dms(dm_callback_t callback, void* user_data) {
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
int subscribed_any = 0;
|
int rc = 0;
|
||||||
|
|
||||||
|
cJSON* filter4 = NULL;
|
||||||
if (need_kind4) {
|
if (need_kind4) {
|
||||||
cJSON* filter4 = cJSON_CreateObject();
|
cJSON* kinds4 = NULL;
|
||||||
cJSON* kinds4 = cJSON_CreateArray();
|
cJSON* p_values4 = NULL;
|
||||||
cJSON* p_values4 = cJSON_CreateArray();
|
|
||||||
|
filter4 = cJSON_CreateObject();
|
||||||
|
kinds4 = cJSON_CreateArray();
|
||||||
|
p_values4 = cJSON_CreateArray();
|
||||||
if (!filter4 || !kinds4 || !p_values4) {
|
if (!filter4 || !kinds4 || !p_values4) {
|
||||||
cJSON_Delete(filter4);
|
cJSON_Delete(filter4);
|
||||||
cJSON_Delete(kinds4);
|
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_AddItemToObject(filter4, "#p", p_values4);
|
||||||
cJSON_AddNumberToObject(filter4, "since", (double)g_start_time);
|
cJSON_AddNumberToObject(filter4, "since", (double)g_start_time);
|
||||||
cJSON_AddNumberToObject(filter4, "limit", 100);
|
cJSON_AddNumberToObject(filter4, "limit", 100);
|
||||||
|
|
||||||
{
|
|
||||||
char* filter_str = cJSON_PrintUnformatted(filter4);
|
|
||||||
DEBUG_TRACE("[didactyl] DEBUG DM subscription filter kind4: %s", filter_str ? filter_str : "<null>");
|
|
||||||
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) {
|
if (need_kind1059) {
|
||||||
time_t since_1059 = g_start_time;
|
cJSON* kinds1059 = NULL;
|
||||||
|
cJSON* p_values1059 = NULL;
|
||||||
|
|
||||||
cJSON* filter1059 = cJSON_CreateObject();
|
filter1059 = cJSON_CreateObject();
|
||||||
cJSON* kinds1059 = cJSON_CreateArray();
|
kinds1059 = cJSON_CreateArray();
|
||||||
cJSON* p_values1059 = cJSON_CreateArray();
|
p_values1059 = cJSON_CreateArray();
|
||||||
if (!filter1059 || !kinds1059 || !p_values1059) {
|
if (!filter1059 || !kinds1059 || !p_values1059) {
|
||||||
|
cJSON_Delete(filter4);
|
||||||
cJSON_Delete(filter1059);
|
cJSON_Delete(filter1059);
|
||||||
cJSON_Delete(kinds1059);
|
cJSON_Delete(kinds1059);
|
||||||
cJSON_Delete(p_values1059);
|
cJSON_Delete(p_values1059);
|
||||||
@@ -2117,21 +2516,15 @@ int nostr_handler_subscribe_dms(dm_callback_t callback, void* user_data) {
|
|||||||
cJSON_AddItemToObject(filter1059, "kinds", kinds1059);
|
cJSON_AddItemToObject(filter1059, "kinds", kinds1059);
|
||||||
cJSON_AddItemToArray(p_values1059, cJSON_CreateString(g_cfg->keys.public_key_hex));
|
cJSON_AddItemToArray(p_values1059, cJSON_CreateString(g_cfg->keys.public_key_hex));
|
||||||
cJSON_AddItemToObject(filter1059, "#p", p_values1059);
|
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);
|
cJSON_AddNumberToObject(filter1059, "limit", 400);
|
||||||
|
|
||||||
{
|
|
||||||
char* filter_str = cJSON_PrintUnformatted(filter1059);
|
|
||||||
DEBUG_TRACE("[didactyl] DEBUG DM subscription filter kind1059: %s", filter_str ? filter_str : "<null>");
|
|
||||||
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(
|
pthread_mutex_lock(&g_subscription_mutex);
|
||||||
g_pool,
|
managed_subscriptions_init_defaults();
|
||||||
(const char**)g_cfg->relays,
|
|
||||||
g_cfg->relay_count,
|
if (managed_subscription_register_locked(MANAGED_SUB_DM_KIND4,
|
||||||
filter1059,
|
need_kind4 ? filter4 : NULL,
|
||||||
on_event,
|
on_event,
|
||||||
on_eose,
|
on_eose,
|
||||||
NULL,
|
NULL,
|
||||||
@@ -2139,19 +2532,30 @@ int nostr_handler_subscribe_dms(dm_callback_t callback, void* user_data) {
|
|||||||
0,
|
0,
|
||||||
NOSTR_POOL_EOSE_FULL_SET,
|
NOSTR_POOL_EOSE_FULL_SET,
|
||||||
30,
|
30,
|
||||||
120);
|
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);
|
cJSON_Delete(filter1059);
|
||||||
if (!sub1059) {
|
|
||||||
fprintf(stderr, "[didactyl] kind1059 DM subscription failed\n");
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
subscribed_any = 1;
|
if (rc != 0) {
|
||||||
DEBUG_TRACE("[didactyl] DEBUG kind1059 DM subscription sub=%p", (void*)sub1059);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!subscribed_any) {
|
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2553,7 +2957,12 @@ int nostr_handler_reconcile_startup_events(void) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!g_system_context) {
|
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++) {
|
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("[]");
|
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) {
|
const char* nostr_handler_get_startup_display_name(void) {
|
||||||
return g_startup_display_name;
|
return g_startup_display_name;
|
||||||
}
|
}
|
||||||
@@ -2986,6 +3422,94 @@ char* nostr_handler_relay_info_json(const char* relay_url) {
|
|||||||
return out;
|
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) {
|
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') {
|
if (!g_cfg || !g_pool || !recipient_pubkey_hex || !message || recipient_pubkey_hex[0] == '\0' || message[0] == '\0') {
|
||||||
return -1;
|
return -1;
|
||||||
@@ -3368,6 +3892,19 @@ int nostr_handler_poll(int timeout_ms) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void nostr_handler_cleanup(void) {
|
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) {
|
if (g_pool) {
|
||||||
nostr_relay_pool_destroy(g_pool);
|
nostr_relay_pool_destroy(g_pool);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ void nostr_handler_refresh_relay_statuses(void);
|
|||||||
int nostr_handler_sync_relays_from_config(void);
|
int nostr_handler_sync_relays_from_config(void);
|
||||||
const char* nostr_handler_get_system_context(void);
|
const char* nostr_handler_get_system_context(void);
|
||||||
char* nostr_handler_get_self_skill_events_json(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);
|
const char* nostr_handler_get_startup_display_name(void);
|
||||||
int nostr_handler_connected_relay_count(void);
|
int nostr_handler_connected_relay_count(void);
|
||||||
char* nostr_handler_get_admin_kind0_context(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);
|
int nostr_handler_is_wot_contact(const char* pubkey_hex);
|
||||||
char* nostr_handler_relay_status_json(void);
|
char* nostr_handler_relay_status_json(void);
|
||||||
char* nostr_handler_relay_info_json(const char* relay_url);
|
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);
|
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);
|
char* nostr_handler_get_dm_history_json(const char* peer_pubkey_hex, int limit);
|
||||||
void nostr_handler_cleanup(void);
|
void nostr_handler_cleanup(void);
|
||||||
|
|||||||
+453
-201
@@ -11,11 +11,16 @@
|
|||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
#include <termios.h>
|
#include <termios.h>
|
||||||
#include <sys/ioctl.h>
|
#include <sys/ioctl.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <limits.h>
|
||||||
|
#include <pwd.h>
|
||||||
|
#include <sys/wait.h>
|
||||||
|
|
||||||
#include "cjson/cJSON.h"
|
#include "cjson/cJSON.h"
|
||||||
#include "llm.h"
|
#include "llm.h"
|
||||||
#include "main.h"
|
#include "main.h"
|
||||||
#include "nostr_handler.h"
|
#include "nostr_handler.h"
|
||||||
|
#include "default_events.h"
|
||||||
#include "../../nostr_core_lib/nostr_core/nostr_core.h"
|
#include "../../nostr_core_lib/nostr_core/nostr_core.h"
|
||||||
|
|
||||||
#define ANSI_UNDERLINE "\x1b[4m"
|
#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) {
|
static int set_default_relays(didactyl_config_t* cfg) {
|
||||||
if (!cfg) return -1;
|
if (!cfg) return -1;
|
||||||
static const char* defaults[] = {
|
|
||||||
"wss://relay.damus.io",
|
|
||||||
"wss://nos.lol",
|
|
||||||
"wss://relay.primal.net"
|
|
||||||
};
|
|
||||||
free_relays_only(cfg);
|
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*));
|
cfg->relays = (char**)calloc((size_t)cfg->relay_count, sizeof(char*));
|
||||||
if (!cfg->relays) {
|
if (!cfg->relays) {
|
||||||
cfg->relay_count = 0;
|
cfg->relay_count = 0;
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
for (int i = 0; i < cfg->relay_count; i++) {
|
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]) {
|
if (!cfg->relays[i]) {
|
||||||
free_relays_only(cfg);
|
free_relays_only(cfg);
|
||||||
return -1;
|
return -1;
|
||||||
@@ -369,131 +369,161 @@ static int build_kind10002_tags_json(const didactyl_config_t* cfg, char** out_ta
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int sync_startup_kind0_event(didactyl_config_t* cfg, const char* name) {
|
static int upsert_startup_event(didactyl_config_t* cfg, int kind, const char* content, const char* tags_json) {
|
||||||
if (!cfg || !name || name[0] == '\0') return -1;
|
if (!cfg || !content || !tags_json) {
|
||||||
|
|
||||||
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;
|
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = 0; i < cfg->startup_event_count; i++) {
|
for (int i = 0; i < cfg->startup_event_count; i++) {
|
||||||
if (cfg->startup_events[i].kind == 0) {
|
startup_event_t* se = &cfg->startup_events[i];
|
||||||
free(cfg->startup_events[i].content);
|
if (se->kind != kind) {
|
||||||
cfg->startup_events[i].content = content_json;
|
continue;
|
||||||
if (!cfg->startup_events[i].tags_json) {
|
}
|
||||||
cfg->startup_events[i].tags_json = strdup("[]");
|
|
||||||
if (!cfg->startup_events[i].tags_json) {
|
char* content_dup = strdup(content);
|
||||||
|
char* tags_dup = strdup(tags_json);
|
||||||
|
if (!content_dup || !tags_dup) {
|
||||||
|
free(content_dup);
|
||||||
|
free(tags_dup);
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
free(se->content);
|
||||||
|
free(se->tags_json);
|
||||||
|
se->content = content_dup;
|
||||||
|
se->tags_json = tags_dup;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
startup_event_t* grown = (startup_event_t*)realloc(cfg->startup_events,
|
startup_event_t* grown = (startup_event_t*)realloc(cfg->startup_events,
|
||||||
(size_t)(cfg->startup_event_count + 1) * sizeof(startup_event_t));
|
(size_t)(cfg->startup_event_count + 1) * sizeof(startup_event_t));
|
||||||
if (!grown) {
|
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;
|
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++;
|
cfg->startup_event_count++;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int sync_startup_kind10002_event(didactyl_config_t* cfg) {
|
static int apply_default_startup_events(didactyl_config_t* cfg, const char* agent_name) {
|
||||||
if (!cfg) return -1;
|
if (!cfg || !agent_name || agent_name[0] == '\0' || cfg->admin.pubkey[0] == '\0') {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < DIDACTYL_DEFAULT_STARTUP_EVENT_KIND_COUNT; i++) {
|
||||||
|
int kind = DIDACTYL_DEFAULT_STARTUP_EVENT_KINDS[i];
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
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;
|
char* tags_json = NULL;
|
||||||
if (build_kind10002_tags_json(cfg, &tags_json) != 0) {
|
if (build_kind10002_tags_json(cfg, &tags_json) != 0 || !tags_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(tags_json);
|
free(tags_json);
|
||||||
return -1;
|
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 < cfg->startup_event_count; i++) {
|
int rc = upsert_startup_event(cfg, kind, DIDACTYL_DEFAULT_KIND10002_CONTENT, tags_json);
|
||||||
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("");
|
|
||||||
}
|
|
||||||
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(tags_json);
|
free(tags_json);
|
||||||
|
if (rc != 0) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
cfg->startup_events = grown;
|
continue;
|
||||||
startup_event_t* se = &cfg->startup_events[cfg->startup_event_count];
|
}
|
||||||
memset(se, 0, sizeof(*se));
|
}
|
||||||
se->kind = 10002;
|
|
||||||
se->content = strdup("");
|
return 0;
|
||||||
se->tags_json = tags_json;
|
}
|
||||||
if (!se->content) {
|
|
||||||
free(se->tags_json);
|
static int configure_default_skill_for_agent(didactyl_config_t* cfg, const char* agent_name) {
|
||||||
se->tags_json = NULL;
|
if (!cfg || !agent_name || agent_name[0] == '\0') {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
cfg->startup_event_count++;
|
|
||||||
|
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->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;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1010,7 +1040,7 @@ static int prompt_model_selection(const cJSON* models,
|
|||||||
static int prompt_admin_pubkey(didactyl_config_t* cfg) {
|
static int prompt_admin_pubkey(didactyl_config_t* cfg) {
|
||||||
char input[WIZARD_LINE_MAX];
|
char input[WIZARD_LINE_MAX];
|
||||||
for (;;) {
|
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) {
|
if (read_line_prompt(" Enter the admin's Nostr public key (npub1... or hex):\n> ", input, sizeof(input)) != 0) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
@@ -1028,7 +1058,7 @@ static int prompt_admin_pubkey(didactyl_config_t* cfg) {
|
|||||||
|
|
||||||
static int prompt_llm_config(didactyl_config_t* cfg) {
|
static int prompt_llm_config(didactyl_config_t* cfg) {
|
||||||
for (;;) {
|
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('1', " ppq.ai");
|
||||||
print_option('2', " Routstr");
|
print_option('2', " Routstr");
|
||||||
print_option('3', " OpenAI-compatible");
|
print_option('3', " OpenAI-compatible");
|
||||||
@@ -1119,25 +1149,7 @@ static int prompt_llm_config(didactyl_config_t* cfg) {
|
|||||||
cJSON_Delete(models);
|
cJSON_Delete(models);
|
||||||
|
|
||||||
int max_tokens = cfg->llm.max_tokens > 0 ? cfg->llm.max_tokens : 512;
|
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;
|
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.provider, sizeof(cfg->llm.provider), "%s", provider);
|
||||||
snprintf(cfg->llm.base_url, sizeof(cfg->llm.base_url), "%s", base_url);
|
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];
|
char input[WIZARD_LINE_MAX];
|
||||||
|
|
||||||
for (;;) {
|
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");
|
fprintf(stderr, " Current relays:\n");
|
||||||
for (int i = 0; i < cfg->relay_count; i++) {
|
for (int i = 0; i < cfg->relay_count; i++) {
|
||||||
fprintf(stderr, " %d. %s\n", i + 1, cfg->relays[i] ? cfg->relays[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) {
|
static int ensure_dir_exists_local(const char* path) {
|
||||||
if (!cfg || !path || path[0] == '\0') return -1;
|
if (!path || path[0] == '\0') return -1;
|
||||||
|
if (mkdir(path, 0755) == 0 || errno == EEXIST) return 0;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
FILE* fp = fopen(path, "w");
|
static int run_command_local(char* const argv[]) {
|
||||||
if (!fp) return -1;
|
pid_t pid = fork();
|
||||||
|
if (pid < 0) return -1;
|
||||||
fprintf(fp, "{\n");
|
if (pid == 0) {
|
||||||
fprintf(fp, " \"key\": {\n");
|
execvp(argv[0], argv);
|
||||||
if (include_nsec) {
|
_exit(127);
|
||||||
fprintf(fp, " \"nsec\": \"%s\"\n", cfg->keys.nsec);
|
|
||||||
}
|
}
|
||||||
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;
|
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;
|
if (!path_out || path_out_size == 0) return -1;
|
||||||
char input[WIZARD_LINE_MAX];
|
char input[WIZARD_LINE_MAX];
|
||||||
if (read_line_prompt("Path [./genesis.jsonc]: ", input, sizeof(input)) != 0) return -1;
|
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) {
|
static int new_agent_identity_step(didactyl_config_t* cfg) {
|
||||||
for (;;) {
|
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('g', "enerate a new Nostr keypair");
|
||||||
print_option('p', "rovide an existing nsec");
|
print_option('p', "rovide an existing nsec");
|
||||||
print_option('b', "ack");
|
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) {
|
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);
|
int rc = new_agent_identity_step(cfg);
|
||||||
if (rc != 0) return rc < 0 ? -1 : 1;
|
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_YELLOW,
|
||||||
ANSI_RESET);
|
ANSI_RESET);
|
||||||
} else {
|
} 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");
|
fprintf(stderr, " Agent kind 10002: %s\n", exists ? "FOUND (may overwrite existing state)" : "Agent npub available for new setup");
|
||||||
if (admin_name_found) {
|
if (admin_name_found) {
|
||||||
fprintf(stderr, " Admin profile: %s\n", admin_name);
|
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;
|
if (prompt_llm_config(cfg) != 0) return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
char agent_name[OW_MAX_NAME_LEN] = {0};
|
if (configure_default_skill_for_agent(cfg, agent_name) != 0) {
|
||||||
render_wizard_page_header("Step 6 of 7", "New Agent Setup -- Agent Profile");
|
fprintf(stderr, "%sFailed to prepare default skill event content.%s\n", ANSI_RED, ANSI_RESET);
|
||||||
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);
|
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sync_startup_kind10002_event(cfg) != 0) {
|
if (apply_default_startup_events(cfg, agent_name) != 0) {
|
||||||
fprintf(stderr, "%sFailed to prepare startup kind10002 event.%s\n", ANSI_RED, ANSI_RESET);
|
fprintf(stderr, "%sFailed to prepare startup events from defaults.%s\n", ANSI_RED, ANSI_RESET);
|
||||||
return -1;
|
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");
|
fprintf(stderr, " DM Protocol: nip04\n\n");
|
||||||
|
|
||||||
print_option('b', "oot the agent now");
|
print_option('b', "oot the agent now");
|
||||||
print_option('w', "rite genesis.jsonc (without nsec) and boot");
|
print_option('i', "nstall dedicated-user systemd service 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('s', "tart over");
|
print_option('s', "tart over");
|
||||||
print_option('q', "uit");
|
print_option('q', "uit");
|
||||||
|
|
||||||
wizard_option_t opts[] = {{'b', ""}, {'w', ""}, {'i', ""}, {'e', ""}, {'s', ""}, {'q', ""}};
|
wizard_option_t opts[] = {{'b', ""}, {'i', ""}, {'s', ""}, {'q', ""}};
|
||||||
char c = read_menu_choice(opts, 6);
|
char c = read_menu_choice(opts, 4);
|
||||||
if (c == 'q') return -1;
|
if (c == 'q') return -1;
|
||||||
if (c == 's') return 2;
|
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;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (c == 'w' || c == 'i' || c == 'e') {
|
if (c == 'i') {
|
||||||
char path[OW_MAX_URL_LEN] = {0};
|
if (install_system_service_with_dedicated_user(cfg, agent_name) != 0) {
|
||||||
if (maybe_prompt_write_path(path, sizeof(path)) != 0) return -1;
|
fprintf(stderr, "%sFailed to install dedicated-user system service.%s\n", ANSI_RED, ANSI_RESET);
|
||||||
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);
|
|
||||||
return -1;
|
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",
|
fprintf(stderr, "%sStep 7 of 7 -- Booting new agent. Check your messages for initial greeting.%s\n",
|
||||||
ANSI_YELLOW,
|
ANSI_YELLOW,
|
||||||
ANSI_RESET);
|
ANSI_RESET);
|
||||||
|
|||||||
@@ -0,0 +1,212 @@
|
|||||||
|
#define _POSIX_C_SOURCE 200809L
|
||||||
|
|
||||||
|
#include "tools_internal.h"
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#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;
|
||||||
|
}
|
||||||
@@ -92,3 +92,105 @@ char* execute_nostr_relay_info(const char* args_json) {
|
|||||||
cJSON_Delete(out);
|
cJSON_Delete(out);
|
||||||
return json;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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) {
|
if (strcmp(tool_name, "nostr_relay_info") == 0) {
|
||||||
return execute_nostr_relay_info(args_json);
|
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) {
|
if (strcmp(tool_name, "nostr_encrypt") == 0) {
|
||||||
return execute_nostr_encrypt(ctx, args_json);
|
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) {
|
if (strcmp(tool_name, "nostr_query") == 0) {
|
||||||
return execute_nostr_query(args_json);
|
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) {
|
if (strcmp(tool_name, "agent_version") == 0) {
|
||||||
return execute_agent_version(args_json);
|
return execute_agent_version(args_json);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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_set(tools_context_t* ctx, const char* args_json);
|
||||||
char* execute_model_list(const char* args_json);
|
char* execute_model_list(const char* args_json);
|
||||||
char* execute_nostr_query(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_pubkey(tools_context_t* ctx, const char* args_json);
|
||||||
char* execute_nostr_npub(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);
|
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_nip05_lookup(const char* args_json);
|
||||||
char* execute_nostr_relay_status(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_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_dm_send(const char* args_json);
|
||||||
char* execute_nostr_encrypt(tools_context_t* ctx, 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);
|
char* execute_nostr_decrypt(tools_context_t* ctx, const char* args_json);
|
||||||
|
|||||||
@@ -410,6 +410,54 @@ char* tools_build_openai_schema_json_legacy(const tools_context_t* ctx) {
|
|||||||
cJSON_AddItemToObject(t15, "function", t15_fn);
|
cJSON_AddItemToObject(t15, "function", t15_fn);
|
||||||
cJSON_AddItemToArray(tools, t15);
|
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 = cJSON_CreateObject();
|
||||||
cJSON* t16_fn = cJSON_CreateObject();
|
cJSON* t16_fn = cJSON_CreateObject();
|
||||||
cJSON* t16_params = 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_AddItemToObject(t49, "function", t49_fn);
|
||||||
cJSON_AddItemToArray(tools, t49);
|
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);
|
char* out = cJSON_PrintUnformatted(tools);
|
||||||
cJSON_Delete(tools);
|
cJSON_Delete(tools);
|
||||||
return out;
|
return out;
|
||||||
|
|||||||
Reference in New Issue
Block a user