Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6b6e25c3f4 | ||
|
|
9a0e90dff0 | ||
|
|
0ce9de002b | ||
|
|
70403854ff |
@@ -97,7 +97,7 @@ RUN if [ "$DEBUG_BUILD" = "true" ]; then \
|
||||
src/main.c src/config.c src/context.c src/llm.c \
|
||||
src/nostr_handler.c src/agent.c src/tools/tools_common.c src/tools/tools_schema.c src/tools/tools_dispatch.c \
|
||||
src/tools/tool_agent.c src/tools/tool_meta.c src/tools/tool_model.c \
|
||||
src/tools/tool_nostr_query.c src/tools/tool_nostr_identity.c src/tools/tool_nostr_social.c \
|
||||
src/tools/tool_nostr_query.c src/tools/tool_nostr_my_events.c src/tools/tool_nostr_identity.c src/tools/tool_nostr_social.c \
|
||||
src/tools/tool_nostr_relay.c src/tools/tool_nostr_dm.c src/tools/tool_admin.c \
|
||||
src/tools/tool_task.c src/tools/tool_nostr_list.c src/tools/tool_local.c \
|
||||
src/tools/tool_skill.c src/tools/tool_nostr_post.c src/tools/tool_memory.c src/tools/tool_config.c src/trigger_manager.c \
|
||||
|
||||
@@ -18,6 +18,7 @@ SRCS = \
|
||||
$(SRC_DIR)/tools/tool_meta.c \
|
||||
$(SRC_DIR)/tools/tool_model.c \
|
||||
$(SRC_DIR)/tools/tool_nostr_query.c \
|
||||
$(SRC_DIR)/tools/tool_nostr_my_events.c \
|
||||
$(SRC_DIR)/tools/tool_nostr_identity.c \
|
||||
$(SRC_DIR)/tools/tool_nostr_social.c \
|
||||
$(SRC_DIR)/tools/tool_nostr_relay.c \
|
||||
|
||||
@@ -55,11 +55,11 @@ Skills compose by adoption-list order (`10123`) and trigger tags carry runtime e
|
||||
|
||||
Didactyl will support local inference, which is very privacy preserving. Remote inference does however have it's advantages, and in those cases Didactyl supports using Bitcoin Lightning and eCash inference providers.
|
||||
|
||||
## Current Status — v0.0.72
|
||||
## Current Status — v0.0.76
|
||||
|
||||
**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.76 — Use sudo-prefixed privileged commands in setup wizard install flow with password prompting and remove root-shell handoff
|
||||
|
||||
- Connects to configured relays with auto-reconnect and relay state transition logging
|
||||
- Publishes configured startup events per relay as each relay becomes connected
|
||||
|
||||
+609
-8016
File diff suppressed because one or more lines are too long
@@ -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}}
|
||||
Executable
+22
@@ -0,0 +1,22 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
SOURCE_BINARY="$SCRIPT_DIR/didactyl_static_x86_64"
|
||||
REMOTE_TARGET="ubuntu@laantungir.net:~/didactyl"
|
||||
|
||||
if ! command -v rsync >/dev/null 2>&1; then
|
||||
echo "ERROR: rsync is not installed or not in PATH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "$SOURCE_BINARY" ]; then
|
||||
echo "ERROR: Source binary not found: $SOURCE_BINARY"
|
||||
echo "Build it first so didactyl_static_x86_64 exists."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Deploying $SOURCE_BINARY to $REMOTE_TARGET"
|
||||
rsync -avz --progress "$SOURCE_BINARY" "$REMOTE_TARGET"
|
||||
echo "Deployment complete."
|
||||
@@ -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* skills = skill_root ? cJSON_GetObjectItemCaseSensitive(skill_root, "skills") : NULL;
|
||||
|
||||
if (!skills || !cJSON_IsArray(skills) || cJSON_GetArraySize(skills) <= 0) {
|
||||
(void)append_textf_local(&out, &cap, &used, "- (none)\n");
|
||||
} else {
|
||||
int available_count = 0;
|
||||
if (skills && cJSON_IsArray(skills) && cJSON_GetArraySize(skills) > 0) {
|
||||
int sn = cJSON_GetArraySize(skills);
|
||||
for (int i = 0; i < sn; i++) {
|
||||
cJSON* row = cJSON_GetArrayItem(skills, i);
|
||||
cJSON* owner = row ? cJSON_GetObjectItemCaseSensitive(row, "owner") : NULL;
|
||||
cJSON* d_tag = row ? cJSON_GetObjectItemCaseSensitive(row, "d_tag") : NULL;
|
||||
cJSON* desc = row ? cJSON_GetObjectItemCaseSensitive(row, "description") : NULL;
|
||||
const char* owner_s = (owner && cJSON_IsString(owner) && owner->valuestring) ? owner->valuestring : "";
|
||||
const char* d_tag_s = (d_tag && cJSON_IsString(d_tag) && d_tag->valuestring) ? d_tag->valuestring : NULL;
|
||||
const char* desc_s = (desc && cJSON_IsString(desc) && desc->valuestring) ? desc->valuestring : "";
|
||||
if (!d_tag_s || d_tag_s[0] == '\0') {
|
||||
|
||||
if (strcmp(owner_s, "agent") != 0 || !d_tag_s || d_tag_s[0] == '\0') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (append_textf_local(&out, &cap, &used, "- %s%s%s\n", d_tag_s, desc_s[0] ? " — " : "", desc_s) != 0) {
|
||||
cJSON_Delete(skill_root);
|
||||
free(skill_list_json);
|
||||
free(out);
|
||||
return NULL;
|
||||
}
|
||||
available_count++;
|
||||
}
|
||||
}
|
||||
|
||||
if (available_count <= 0) {
|
||||
(void)append_textf_local(&out, &cap, &used, "- (none)\n");
|
||||
}
|
||||
|
||||
cJSON_Delete(skill_root);
|
||||
free(skill_list_json);
|
||||
|
||||
if (append_textf_local(&out, &cap, &used, "\nADOPTED SKILLS\n") != 0) {
|
||||
free(out);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
char* adopted_json = tools_execute(&g_tools_ctx, "adopted_skills", "{}");
|
||||
cJSON* adopted_root = adopted_json ? cJSON_Parse(adopted_json) : NULL;
|
||||
cJSON* adoption_events_json = adopted_root ? cJSON_GetObjectItemCaseSensitive(adopted_root, "adoption_events_json") : NULL;
|
||||
cJSON* adoption_events = (adoption_events_json && cJSON_IsString(adoption_events_json) && adoption_events_json->valuestring)
|
||||
? cJSON_Parse(adoption_events_json->valuestring)
|
||||
: NULL;
|
||||
|
||||
int adopted_count = 0;
|
||||
if (adoption_events && cJSON_IsArray(adoption_events) && cJSON_GetArraySize(adoption_events) > 0) {
|
||||
cJSON* ev0 = cJSON_GetArrayItem(adoption_events, 0);
|
||||
cJSON* tags = ev0 ? cJSON_GetObjectItemCaseSensitive(ev0, "tags") : NULL;
|
||||
if (tags && cJSON_IsArray(tags)) {
|
||||
int tn = cJSON_GetArraySize(tags);
|
||||
for (int i = 0; i < tn; i++) {
|
||||
cJSON* tag = cJSON_GetArrayItem(tags, i);
|
||||
if (!tag || !cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
cJSON* k = cJSON_GetArrayItem(tag, 0);
|
||||
cJSON* v = cJSON_GetArrayItem(tag, 1);
|
||||
if (!k || !v || !cJSON_IsString(k) || !k->valuestring ||
|
||||
!cJSON_IsString(v) || !v->valuestring) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strcmp(k->valuestring, "a") != 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const char* addr = v->valuestring;
|
||||
const char* d_tag = strrchr(addr, ':');
|
||||
const char* label = (d_tag && d_tag[1] != '\0') ? (d_tag + 1) : addr;
|
||||
|
||||
if (append_textf_local(&out, &cap, &used, "- %s\n", label) != 0) {
|
||||
cJSON_Delete(adoption_events);
|
||||
cJSON_Delete(adopted_root);
|
||||
free(adopted_json);
|
||||
free(out);
|
||||
return NULL;
|
||||
}
|
||||
adopted_count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (adopted_count <= 0) {
|
||||
(void)append_textf_local(&out, &cap, &used, "- (none)\n");
|
||||
}
|
||||
|
||||
cJSON_Delete(adoption_events);
|
||||
cJSON_Delete(adopted_root);
|
||||
free(adopted_json);
|
||||
return out;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 "[[\"d\",\"didactyl-default\"],[\"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* skills = skill_root ? cJSON_GetObjectItemCaseSensitive(skill_root, "skills") : NULL;
|
||||
|
||||
if (!skills || !cJSON_IsArray(skills) || cJSON_GetArraySize(skills) <= 0) {
|
||||
(void)append_textf_http(&out, &cap, &used, "- (none)\n");
|
||||
} else {
|
||||
int available_count = 0;
|
||||
if (skills && cJSON_IsArray(skills) && cJSON_GetArraySize(skills) > 0) {
|
||||
int sn = cJSON_GetArraySize(skills);
|
||||
for (int i = 0; i < sn; i++) {
|
||||
cJSON* row = cJSON_GetArrayItem(skills, i);
|
||||
cJSON* owner = row ? cJSON_GetObjectItemCaseSensitive(row, "owner") : NULL;
|
||||
cJSON* d_tag = row ? cJSON_GetObjectItemCaseSensitive(row, "d_tag") : NULL;
|
||||
cJSON* desc = row ? cJSON_GetObjectItemCaseSensitive(row, "description") : NULL;
|
||||
const char* owner_s = (owner && cJSON_IsString(owner) && owner->valuestring) ? owner->valuestring : "";
|
||||
const char* d_tag_s = (d_tag && cJSON_IsString(d_tag) && d_tag->valuestring) ? d_tag->valuestring : NULL;
|
||||
const char* desc_s = (desc && cJSON_IsString(desc) && desc->valuestring) ? desc->valuestring : "";
|
||||
if (!d_tag_s || d_tag_s[0] == '\0') {
|
||||
|
||||
if (strcmp(owner_s, "agent") != 0 || !d_tag_s || d_tag_s[0] == '\0') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (append_textf_http(&out, &cap, &used, "- %s%s%s\n", d_tag_s, desc_s[0] ? " — " : "", desc_s) != 0) {
|
||||
cJSON_Delete(skill_root);
|
||||
free(skill_list_json);
|
||||
free(out);
|
||||
return NULL;
|
||||
}
|
||||
available_count++;
|
||||
}
|
||||
}
|
||||
|
||||
if (available_count <= 0) {
|
||||
(void)append_textf_http(&out, &cap, &used, "- (none)\n");
|
||||
}
|
||||
|
||||
cJSON_Delete(skill_root);
|
||||
free(skill_list_json);
|
||||
|
||||
if (append_textf_http(&out, &cap, &used, "\nADOPTED SKILLS\n") != 0) {
|
||||
free(out);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
char* adopted_json = tools_execute(g_api_ctx.tools_ctx, "adopted_skills", "{}");
|
||||
cJSON* adopted_root = adopted_json ? cJSON_Parse(adopted_json) : NULL;
|
||||
cJSON* adoption_events_json = adopted_root ? cJSON_GetObjectItemCaseSensitive(adopted_root, "adoption_events_json") : NULL;
|
||||
cJSON* adoption_events = (adoption_events_json && cJSON_IsString(adoption_events_json) && adoption_events_json->valuestring)
|
||||
? cJSON_Parse(adoption_events_json->valuestring)
|
||||
: NULL;
|
||||
|
||||
int adopted_count = 0;
|
||||
if (adoption_events && cJSON_IsArray(adoption_events) && cJSON_GetArraySize(adoption_events) > 0) {
|
||||
cJSON* ev0 = cJSON_GetArrayItem(adoption_events, 0);
|
||||
cJSON* tags = ev0 ? cJSON_GetObjectItemCaseSensitive(ev0, "tags") : NULL;
|
||||
if (tags && cJSON_IsArray(tags)) {
|
||||
int tn = cJSON_GetArraySize(tags);
|
||||
for (int i = 0; i < tn; i++) {
|
||||
cJSON* tag = cJSON_GetArrayItem(tags, i);
|
||||
if (!tag || !cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
cJSON* k = cJSON_GetArrayItem(tag, 0);
|
||||
cJSON* v = cJSON_GetArrayItem(tag, 1);
|
||||
if (!k || !v || !cJSON_IsString(k) || !k->valuestring ||
|
||||
!cJSON_IsString(v) || !v->valuestring) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strcmp(k->valuestring, "a") != 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const char* addr = v->valuestring;
|
||||
const char* d_tag = strrchr(addr, ':');
|
||||
const char* label = (d_tag && d_tag[1] != '\0') ? (d_tag + 1) : addr;
|
||||
|
||||
if (append_textf_http(&out, &cap, &used, "- %s\n", label) != 0) {
|
||||
cJSON_Delete(adoption_events);
|
||||
cJSON_Delete(adopted_root);
|
||||
free(adopted_json);
|
||||
free(out);
|
||||
return NULL;
|
||||
}
|
||||
adopted_count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (adopted_count <= 0) {
|
||||
(void)append_textf_http(&out, &cap, &used, "- (none)\n");
|
||||
}
|
||||
|
||||
cJSON_Delete(adoption_events);
|
||||
cJSON_Delete(adopted_root);
|
||||
free(adopted_json);
|
||||
return out;
|
||||
}
|
||||
|
||||
|
||||
+7
-1
@@ -1166,8 +1166,14 @@ int main(int argc, char** argv) {
|
||||
startup_step_ok(7, "Reconcile/persist startup state", NULL);
|
||||
|
||||
const char* system_context = nostr_handler_get_system_context();
|
||||
char system_context_fallback[512] = {0};
|
||||
if (!system_context || system_context[0] == '\0') {
|
||||
system_context = "You are Didactyl, a sovereign AI agent living on Nostr.";
|
||||
const char* startup_name = nostr_handler_get_startup_display_name();
|
||||
snprintf(system_context_fallback,
|
||||
sizeof(system_context_fallback),
|
||||
"You are %s, a sovereign AI agent living on Nostr.",
|
||||
(startup_name && startup_name[0] != '\0') ? startup_name : "the agent");
|
||||
system_context = system_context_fallback;
|
||||
}
|
||||
|
||||
startup_step_begin(8, "Initialize agent");
|
||||
|
||||
+2
-2
@@ -12,8 +12,8 @@
|
||||
// Using DIDACTYL_ prefix to avoid conflicts with nostr_core_lib VERSION macros
|
||||
#define DIDACTYL_VERSION_MAJOR 0
|
||||
#define DIDACTYL_VERSION_MINOR 0
|
||||
#define DIDACTYL_VERSION_PATCH 72
|
||||
#define DIDACTYL_VERSION "v0.0.72"
|
||||
#define DIDACTYL_VERSION_PATCH 76
|
||||
#define DIDACTYL_VERSION "v0.0.76"
|
||||
|
||||
// Agent metadata
|
||||
#define DIDACTYL_NAME "Didactyl"
|
||||
|
||||
+717
-180
File diff suppressed because it is too large
Load Diff
@@ -63,6 +63,7 @@ void nostr_handler_refresh_relay_statuses(void);
|
||||
int nostr_handler_sync_relays_from_config(void);
|
||||
const char* nostr_handler_get_system_context(void);
|
||||
char* nostr_handler_get_self_skill_events_json(void);
|
||||
int nostr_handler_is_event_in_self_cache(const char* event_id_hex);
|
||||
const char* nostr_handler_get_startup_display_name(void);
|
||||
int nostr_handler_connected_relay_count(void);
|
||||
char* nostr_handler_get_admin_kind0_context(void);
|
||||
@@ -76,6 +77,8 @@ char* nostr_handler_get_agent_kind1_notes_context(void);
|
||||
int nostr_handler_is_wot_contact(const char* pubkey_hex);
|
||||
char* nostr_handler_relay_status_json(void);
|
||||
char* nostr_handler_relay_info_json(const char* relay_url);
|
||||
char* nostr_handler_subscription_status_json(void);
|
||||
int nostr_handler_subscription_set_json(const char* name, cJSON* filter, int enabled, int enabled_set);
|
||||
int nostr_handler_send_dm_nip17(const char* recipient_pubkey_hex, const char* message, const char* subject);
|
||||
char* nostr_handler_get_dm_history_json(const char* peer_pubkey_hex, int limit);
|
||||
void nostr_handler_cleanup(void);
|
||||
|
||||
+499
-204
@@ -11,11 +11,16 @@
|
||||
#include <unistd.h>
|
||||
#include <termios.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <sys/stat.h>
|
||||
#include <limits.h>
|
||||
#include <pwd.h>
|
||||
#include <sys/wait.h>
|
||||
|
||||
#include "cjson/cJSON.h"
|
||||
#include "llm.h"
|
||||
#include "main.h"
|
||||
#include "nostr_handler.h"
|
||||
#include "default_events.h"
|
||||
#include "../../nostr_core_lib/nostr_core/nostr_core.h"
|
||||
|
||||
#define ANSI_UNDERLINE "\x1b[4m"
|
||||
@@ -321,20 +326,15 @@ static void free_relays_only(didactyl_config_t* cfg) {
|
||||
|
||||
static int set_default_relays(didactyl_config_t* cfg) {
|
||||
if (!cfg) return -1;
|
||||
static const char* defaults[] = {
|
||||
"wss://relay.damus.io",
|
||||
"wss://nos.lol",
|
||||
"wss://relay.primal.net"
|
||||
};
|
||||
free_relays_only(cfg);
|
||||
cfg->relay_count = (int)(sizeof(defaults) / sizeof(defaults[0]));
|
||||
cfg->relay_count = DIDACTYL_DEFAULT_RELAY_COUNT;
|
||||
cfg->relays = (char**)calloc((size_t)cfg->relay_count, sizeof(char*));
|
||||
if (!cfg->relays) {
|
||||
cfg->relay_count = 0;
|
||||
return -1;
|
||||
}
|
||||
for (int i = 0; i < cfg->relay_count; i++) {
|
||||
cfg->relays[i] = strdup(defaults[i]);
|
||||
cfg->relays[i] = strdup(DIDACTYL_DEFAULT_RELAYS[i]);
|
||||
if (!cfg->relays[i]) {
|
||||
free_relays_only(cfg);
|
||||
return -1;
|
||||
@@ -369,131 +369,161 @@ static int build_kind10002_tags_json(const didactyl_config_t* cfg, char** out_ta
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int sync_startup_kind0_event(didactyl_config_t* cfg, const char* name) {
|
||||
if (!cfg || !name || name[0] == '\0') return -1;
|
||||
|
||||
cJSON* profile = cJSON_CreateObject();
|
||||
if (!profile) return -1;
|
||||
cJSON_AddStringToObject(profile, "name", name);
|
||||
cJSON_AddStringToObject(profile, "display_name", name);
|
||||
|
||||
char* content_json = cJSON_PrintUnformatted(profile);
|
||||
cJSON_Delete(profile);
|
||||
if (!content_json) return -1;
|
||||
|
||||
if (!cfg->startup_events || cfg->startup_event_count <= 0) {
|
||||
cfg->startup_events = (startup_event_t*)calloc(1U, sizeof(startup_event_t));
|
||||
if (!cfg->startup_events) {
|
||||
free(content_json);
|
||||
return -1;
|
||||
}
|
||||
cfg->startup_event_count = 1;
|
||||
cfg->startup_events[0].kind = 0;
|
||||
cfg->startup_events[0].content = content_json;
|
||||
cfg->startup_events[0].tags_json = strdup("[]");
|
||||
if (!cfg->startup_events[0].tags_json) {
|
||||
free(cfg->startup_events[0].content);
|
||||
free(cfg->startup_events);
|
||||
cfg->startup_events = NULL;
|
||||
cfg->startup_event_count = 0;
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
static int upsert_startup_event(didactyl_config_t* cfg, int kind, const char* content, const char* tags_json) {
|
||||
if (!cfg || !content || !tags_json) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
for (int i = 0; i < cfg->startup_event_count; i++) {
|
||||
if (cfg->startup_events[i].kind == 0) {
|
||||
free(cfg->startup_events[i].content);
|
||||
cfg->startup_events[i].content = content_json;
|
||||
if (!cfg->startup_events[i].tags_json) {
|
||||
cfg->startup_events[i].tags_json = strdup("[]");
|
||||
if (!cfg->startup_events[i].tags_json) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
startup_event_t* se = &cfg->startup_events[i];
|
||||
if (se->kind != kind) {
|
||||
continue;
|
||||
}
|
||||
|
||||
char* content_dup = strdup(content);
|
||||
char* tags_dup = strdup(tags_json);
|
||||
if (!content_dup || !tags_dup) {
|
||||
free(content_dup);
|
||||
free(tags_dup);
|
||||
return -1;
|
||||
}
|
||||
|
||||
free(se->content);
|
||||
free(se->tags_json);
|
||||
se->content = content_dup;
|
||||
se->tags_json = tags_dup;
|
||||
return 0;
|
||||
}
|
||||
|
||||
startup_event_t* grown = (startup_event_t*)realloc(cfg->startup_events,
|
||||
(size_t)(cfg->startup_event_count + 1) * sizeof(startup_event_t));
|
||||
if (!grown) {
|
||||
free(content_json);
|
||||
return -1;
|
||||
}
|
||||
cfg->startup_events = grown;
|
||||
|
||||
startup_event_t* se = &cfg->startup_events[cfg->startup_event_count];
|
||||
memset(se, 0, sizeof(*se));
|
||||
se->kind = kind;
|
||||
se->content = strdup(content);
|
||||
se->tags_json = strdup(tags_json);
|
||||
if (!se->content || !se->tags_json) {
|
||||
free(se->content);
|
||||
free(se->tags_json);
|
||||
se->content = NULL;
|
||||
se->tags_json = NULL;
|
||||
return -1;
|
||||
}
|
||||
|
||||
cfg->startup_events = grown;
|
||||
startup_event_t* se = &cfg->startup_events[cfg->startup_event_count];
|
||||
memset(se, 0, sizeof(*se));
|
||||
se->kind = 0;
|
||||
se->content = content_json;
|
||||
se->tags_json = strdup("[]");
|
||||
if (!se->tags_json) {
|
||||
free(se->content);
|
||||
se->content = NULL;
|
||||
return -1;
|
||||
}
|
||||
cfg->startup_event_count++;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int sync_startup_kind10002_event(didactyl_config_t* cfg) {
|
||||
if (!cfg) return -1;
|
||||
|
||||
char* tags_json = NULL;
|
||||
if (build_kind10002_tags_json(cfg, &tags_json) != 0) {
|
||||
static int apply_default_startup_events(didactyl_config_t* cfg, const char* agent_name) {
|
||||
if (!cfg || !agent_name || agent_name[0] == '\0' || cfg->admin.pubkey[0] == '\0') {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!cfg->startup_events || cfg->startup_event_count <= 0) {
|
||||
cfg->startup_events = (startup_event_t*)calloc(1U, sizeof(startup_event_t));
|
||||
if (!cfg->startup_events) {
|
||||
free(tags_json);
|
||||
return -1;
|
||||
}
|
||||
cfg->startup_event_count = 1;
|
||||
cfg->startup_events[0].kind = 10002;
|
||||
cfg->startup_events[0].content = strdup("");
|
||||
cfg->startup_events[0].tags_json = tags_json;
|
||||
if (!cfg->startup_events[0].content) {
|
||||
free(cfg->startup_events[0].tags_json);
|
||||
free(cfg->startup_events);
|
||||
cfg->startup_events = NULL;
|
||||
cfg->startup_event_count = 0;
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
for (int i = 0; i < DIDACTYL_DEFAULT_STARTUP_EVENT_KIND_COUNT; i++) {
|
||||
int kind = DIDACTYL_DEFAULT_STARTUP_EVENT_KINDS[i];
|
||||
|
||||
for (int i = 0; i < cfg->startup_event_count; i++) {
|
||||
if (cfg->startup_events[i].kind == 10002) {
|
||||
free(cfg->startup_events[i].tags_json);
|
||||
cfg->startup_events[i].tags_json = tags_json;
|
||||
if (!cfg->startup_events[i].content) {
|
||||
cfg->startup_events[i].content = strdup("");
|
||||
if (kind == DIDACTYL_STARTUP_KIND_PROFILE) {
|
||||
int needed = snprintf(NULL,
|
||||
0,
|
||||
DIDACTYL_DEFAULT_KIND0_PROFILE_JSON_TEMPLATE,
|
||||
agent_name,
|
||||
agent_name);
|
||||
if (needed <= 0) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
char* content_json = (char*)malloc((size_t)needed + 1U);
|
||||
if (!content_json) {
|
||||
return -1;
|
||||
}
|
||||
snprintf(content_json,
|
||||
(size_t)needed + 1U,
|
||||
DIDACTYL_DEFAULT_KIND0_PROFILE_JSON_TEMPLATE,
|
||||
agent_name,
|
||||
agent_name);
|
||||
|
||||
int rc = upsert_startup_event(cfg, kind, content_json, "[]");
|
||||
free(content_json);
|
||||
if (rc != 0) {
|
||||
return -1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (kind == DIDACTYL_STARTUP_KIND_CONTACTS) {
|
||||
int needed = snprintf(NULL, 0, DIDACTYL_DEFAULT_KIND3_TAGS_JSON_TEMPLATE, cfg->admin.pubkey);
|
||||
if (needed <= 0) {
|
||||
return -1;
|
||||
}
|
||||
char* tags_json = (char*)malloc((size_t)needed + 1U);
|
||||
if (!tags_json) {
|
||||
return -1;
|
||||
}
|
||||
snprintf(tags_json, (size_t)needed + 1U, DIDACTYL_DEFAULT_KIND3_TAGS_JSON_TEMPLATE, cfg->admin.pubkey);
|
||||
|
||||
int rc = upsert_startup_event(cfg, kind, DIDACTYL_DEFAULT_KIND3_CONTENT, tags_json);
|
||||
free(tags_json);
|
||||
if (rc != 0) {
|
||||
return -1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (kind == DIDACTYL_STARTUP_KIND_RELAYS) {
|
||||
char* tags_json = NULL;
|
||||
if (build_kind10002_tags_json(cfg, &tags_json) != 0 || !tags_json) {
|
||||
free(tags_json);
|
||||
return -1;
|
||||
}
|
||||
|
||||
int rc = upsert_startup_event(cfg, kind, DIDACTYL_DEFAULT_KIND10002_CONTENT, tags_json);
|
||||
free(tags_json);
|
||||
if (rc != 0) {
|
||||
return -1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
startup_event_t* grown = (startup_event_t*)realloc(cfg->startup_events,
|
||||
(size_t)(cfg->startup_event_count + 1) * sizeof(startup_event_t));
|
||||
if (!grown) {
|
||||
free(tags_json);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int configure_default_skill_for_agent(didactyl_config_t* cfg, const char* agent_name) {
|
||||
if (!cfg || !agent_name || agent_name[0] == '\0') {
|
||||
return -1;
|
||||
}
|
||||
cfg->startup_events = grown;
|
||||
startup_event_t* se = &cfg->startup_events[cfg->startup_event_count];
|
||||
memset(se, 0, sizeof(*se));
|
||||
se->kind = 10002;
|
||||
se->content = strdup("");
|
||||
se->tags_json = tags_json;
|
||||
if (!se->content) {
|
||||
free(se->tags_json);
|
||||
se->tags_json = NULL;
|
||||
|
||||
free(cfg->default_skill.content);
|
||||
cfg->default_skill.content = NULL;
|
||||
free(cfg->default_skill.tags_json);
|
||||
cfg->default_skill.tags_json = NULL;
|
||||
|
||||
cfg->default_skill.kind = DIDACTYL_DEFAULT_SKILL_KIND;
|
||||
snprintf(cfg->default_skill.d_tag, sizeof(cfg->default_skill.d_tag), "%s", DIDACTYL_DEFAULT_SKILL_D_TAG);
|
||||
|
||||
int needed = snprintf(NULL, 0, DIDACTYL_DEFAULT_SKILL_TEMPLATE, agent_name, agent_name);
|
||||
if (needed <= 0) {
|
||||
return -1;
|
||||
}
|
||||
cfg->startup_event_count++;
|
||||
|
||||
cfg->default_skill.content = (char*)malloc((size_t)needed + 1U);
|
||||
if (!cfg->default_skill.content) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
snprintf(cfg->default_skill.content, (size_t)needed + 1U, DIDACTYL_DEFAULT_SKILL_TEMPLATE, agent_name, agent_name);
|
||||
|
||||
cfg->default_skill.tags_json = strdup(DIDACTYL_DEFAULT_SKILL_TAGS_JSON);
|
||||
if (!cfg->default_skill.tags_json) {
|
||||
free(cfg->default_skill.content);
|
||||
cfg->default_skill.content = NULL;
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -1010,7 +1040,7 @@ static int prompt_model_selection(const cJSON* models,
|
||||
static int prompt_admin_pubkey(didactyl_config_t* cfg) {
|
||||
char input[WIZARD_LINE_MAX];
|
||||
for (;;) {
|
||||
render_wizard_page_header("Step 2 of 7", "New Agent Setup -- Administrator");
|
||||
render_wizard_page_header("Step 3 of 7", "New Agent Setup -- Administrator");
|
||||
if (read_line_prompt(" Enter the admin's Nostr public key (npub1... or hex):\n> ", input, sizeof(input)) != 0) {
|
||||
return -1;
|
||||
}
|
||||
@@ -1028,7 +1058,7 @@ static int prompt_admin_pubkey(didactyl_config_t* cfg) {
|
||||
|
||||
static int prompt_llm_config(didactyl_config_t* cfg) {
|
||||
for (;;) {
|
||||
render_wizard_page_header("Step 4 of 7", "New Agent Setup -- LLM Provider");
|
||||
render_wizard_page_header("Step 5 of 7", "New Agent Setup -- LLM Provider");
|
||||
print_option('1', " ppq.ai");
|
||||
print_option('2', " Routstr");
|
||||
print_option('3', " OpenAI-compatible");
|
||||
@@ -1119,25 +1149,7 @@ static int prompt_llm_config(didactyl_config_t* cfg) {
|
||||
cJSON_Delete(models);
|
||||
|
||||
int max_tokens = cfg->llm.max_tokens > 0 ? cfg->llm.max_tokens : 512;
|
||||
char max_tokens_s[32] = {0};
|
||||
char prompt[256];
|
||||
snprintf(prompt, sizeof(prompt), " Max Tokens [%d]: ", max_tokens);
|
||||
if (read_line_prompt(prompt, max_tokens_s, sizeof(max_tokens_s)) != 0) return -1;
|
||||
if (line_is_quit(max_tokens_s)) return -1;
|
||||
if (max_tokens_s[0] != '\0') {
|
||||
int mt = atoi(max_tokens_s);
|
||||
if (mt > 0) max_tokens = mt;
|
||||
}
|
||||
|
||||
double temperature = cfg->llm.temperature > 0.0 ? cfg->llm.temperature : 0.7;
|
||||
char temperature_s[32] = {0};
|
||||
snprintf(prompt, sizeof(prompt), " Temperature [%.1f]: ", temperature);
|
||||
if (read_line_prompt(prompt, temperature_s, sizeof(temperature_s)) != 0) return -1;
|
||||
if (line_is_quit(temperature_s)) return -1;
|
||||
if (temperature_s[0] != '\0') {
|
||||
double t = atof(temperature_s);
|
||||
if (t >= 0.0 && t <= 2.0) temperature = t;
|
||||
}
|
||||
|
||||
snprintf(cfg->llm.provider, sizeof(cfg->llm.provider), "%s", provider);
|
||||
snprintf(cfg->llm.base_url, sizeof(cfg->llm.base_url), "%s", base_url);
|
||||
@@ -1171,7 +1183,7 @@ static int prompt_relay_configuration(didactyl_config_t* cfg) {
|
||||
char input[WIZARD_LINE_MAX];
|
||||
|
||||
for (;;) {
|
||||
render_wizard_page_header("Step 5 of 7", "New Agent Setup -- Relay Configuration");
|
||||
render_wizard_page_header("Step 6 of 7", "New Agent Setup -- Relay Configuration");
|
||||
fprintf(stderr, " Current relays:\n");
|
||||
for (int i = 0; i < cfg->relay_count; i++) {
|
||||
fprintf(stderr, " %d. %s\n", i + 1, cfg->relays[i] ? cfg->relays[i] : "");
|
||||
@@ -1223,53 +1235,351 @@ static int prompt_relay_configuration(didactyl_config_t* cfg) {
|
||||
}
|
||||
}
|
||||
|
||||
static int write_genesis_file(const didactyl_config_t* cfg, const char* path, int include_nsec) {
|
||||
if (!cfg || !path || path[0] == '\0') return -1;
|
||||
|
||||
FILE* fp = fopen(path, "w");
|
||||
if (!fp) return -1;
|
||||
|
||||
fprintf(fp, "{\n");
|
||||
fprintf(fp, " \"key\": {\n");
|
||||
if (include_nsec) {
|
||||
fprintf(fp, " \"nsec\": \"%s\"\n", cfg->keys.nsec);
|
||||
static int run_command_local(char* const argv[]) {
|
||||
pid_t pid = fork();
|
||||
if (pid < 0) return -1;
|
||||
if (pid == 0) {
|
||||
execvp(argv[0], argv);
|
||||
_exit(127);
|
||||
}
|
||||
fprintf(fp, " },\n");
|
||||
fprintf(fp, " \"admin\": {\n");
|
||||
fprintf(fp, " \"pubkey\": \"%s\"\n", cfg->admin.pubkey);
|
||||
fprintf(fp, " },\n");
|
||||
fprintf(fp, " \"dm_protocol\": \"nip04\",\n");
|
||||
fprintf(fp, " \"llm\": {\n");
|
||||
fprintf(fp, " \"provider\": \"%s\",\n", cfg->llm.provider[0] ? cfg->llm.provider : "openai");
|
||||
fprintf(fp, " \"api_key\": \"%s\",\n", cfg->llm.api_key);
|
||||
fprintf(fp, " \"model\": \"%s\",\n", cfg->llm.model);
|
||||
fprintf(fp, " \"base_url\": \"%s\",\n", cfg->llm.base_url);
|
||||
fprintf(fp, " \"max_tokens\": %d,\n", cfg->llm.max_tokens > 0 ? cfg->llm.max_tokens : 512);
|
||||
fprintf(fp, " \"temperature\": %.2f\n", cfg->llm.temperature > 0.0 ? cfg->llm.temperature : 0.7);
|
||||
fprintf(fp, " },\n");
|
||||
fprintf(fp, " \"api\": {\n");
|
||||
fprintf(fp, " \"enabled\": true,\n");
|
||||
fprintf(fp, " \"port\": %d,\n", cfg->api.port > 0 ? cfg->api.port : 8484);
|
||||
fprintf(fp, " \"bind_address\": \"%s\"\n", cfg->api.bind_address[0] ? cfg->api.bind_address : "127.0.0.1");
|
||||
fprintf(fp, " },\n");
|
||||
fprintf(fp, " \"startup_events\": [\n");
|
||||
fprintf(fp, " {\n");
|
||||
fprintf(fp, " \"kind\": 10002,\n");
|
||||
fprintf(fp, " \"content\": \"\",\n");
|
||||
fprintf(fp, " \"tags\": [\n");
|
||||
for (int i = 0; i < cfg->relay_count; i++) {
|
||||
fprintf(fp, " [\"r\", \"%s\"]%s\n", cfg->relays[i], (i + 1 < cfg->relay_count) ? "," : "");
|
||||
}
|
||||
fprintf(fp, " ]\n");
|
||||
fprintf(fp, " }\n");
|
||||
fprintf(fp, " ]\n");
|
||||
fprintf(fp, "}\n");
|
||||
|
||||
fclose(fp);
|
||||
int status = 0;
|
||||
if (waitpid(pid, &status, 0) < 0) return -1;
|
||||
if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) return -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int maybe_prompt_write_path(char* path_out, size_t path_out_size) {
|
||||
static int run_privileged_command_local(char* const argv[]) {
|
||||
if (!argv || !argv[0]) return -1;
|
||||
|
||||
if (geteuid() == 0) {
|
||||
return run_command_local(argv);
|
||||
}
|
||||
|
||||
char* sudo_argv[64] = {0};
|
||||
sudo_argv[0] = "sudo";
|
||||
|
||||
int i = 0;
|
||||
for (; argv[i] && i < 62; i++) {
|
||||
sudo_argv[i + 1] = argv[i];
|
||||
}
|
||||
|
||||
if (argv[i] != NULL) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
sudo_argv[i + 1] = NULL;
|
||||
return run_command_local(sudo_argv);
|
||||
}
|
||||
|
||||
static int write_file_privileged_local(const char* path, const char* content, unsigned int mode) {
|
||||
if (!path || path[0] == '\0' || !content) return -1;
|
||||
|
||||
char tmp_template[] = "/tmp/didactyl-wizard-XXXXXX";
|
||||
int fd = mkstemp(tmp_template);
|
||||
if (fd < 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
FILE* fp = fdopen(fd, "w");
|
||||
if (!fp) {
|
||||
close(fd);
|
||||
unlink(tmp_template);
|
||||
return -1;
|
||||
}
|
||||
|
||||
size_t len = strlen(content);
|
||||
int ok = (fwrite(content, 1, len, fp) == len) ? 1 : 0;
|
||||
if (fclose(fp) != 0) {
|
||||
ok = 0;
|
||||
}
|
||||
|
||||
if (!ok) {
|
||||
unlink(tmp_template);
|
||||
return -1;
|
||||
}
|
||||
|
||||
char mode_buf[16] = {0};
|
||||
snprintf(mode_buf, sizeof(mode_buf), "%04o", mode & 07777U);
|
||||
|
||||
char* install_argv[] = {"install", "-m", mode_buf, tmp_template, (char*)path, NULL};
|
||||
int rc = run_privileged_command_local(install_argv);
|
||||
|
||||
unlink(tmp_template);
|
||||
return rc;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
char content[4096] = {0};
|
||||
int n = snprintf(content,
|
||||
sizeof(content),
|
||||
"# 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);
|
||||
if (n < 0 || (size_t)n >= sizeof(content)) {
|
||||
fprintf(stderr, "%sFailed to build sudoers content for %s.%s\n", ANSI_RED, service_user, ANSI_RESET);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (write_file_privileged_local(sudoers_path, content, 0440U) != 0) {
|
||||
fprintf(stderr, "%sFailed to write 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, " Requesting sudo authentication for system install...\n");
|
||||
char* sudo_validate_argv[] = {"sudo", "-v", NULL};
|
||||
if (run_command_local(sudo_validate_argv) != 0) {
|
||||
fprintf(stderr, "%sSudo authentication failed. System install canceled.%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_privileged_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;
|
||||
}
|
||||
|
||||
/* --- 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);
|
||||
char* install_bin_argv[] = {"install", "-m", "0755", self_path, bin_path, NULL};
|
||||
if (run_privileged_command_local(install_bin_argv) != 0) {
|
||||
fprintf(stderr, "%sFailed to copy binary to %s.%s\n", ANSI_RED, bin_path, ANSI_RESET);
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* --- Set ownership on home directory and installed binary --- */
|
||||
char owner_spec[64] = {0};
|
||||
snprintf(owner_spec, sizeof(owner_spec), "%lu:%lu", (unsigned long)pw->pw_uid, (unsigned long)pw->pw_gid);
|
||||
|
||||
char* chown_home_argv[] = {"chown", owner_spec, (char*)home, NULL};
|
||||
char* chmod_home_argv[] = {"chmod", "0750", (char*)home, NULL};
|
||||
char* chown_bin_argv[] = {"chown", owner_spec, bin_path, NULL};
|
||||
|
||||
if (run_privileged_command_local(chown_home_argv) != 0 ||
|
||||
run_privileged_command_local(chmod_home_argv) != 0 ||
|
||||
run_privileged_command_local(chown_bin_argv) != 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);
|
||||
|
||||
char env_line[PATH_MAX + 64] = {0};
|
||||
if (ca_bundle) {
|
||||
snprintf(env_line, sizeof(env_line), "Environment=SSL_CERT_FILE=%s\\n", ca_bundle);
|
||||
}
|
||||
|
||||
char service_content[8192] = {0};
|
||||
int svc_len = snprintf(service_content,
|
||||
sizeof(service_content),
|
||||
"[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"
|
||||
"%s"
|
||||
"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,
|
||||
service_user,
|
||||
service_user,
|
||||
home,
|
||||
bin_path,
|
||||
cfg->keys.nsec,
|
||||
env_line,
|
||||
service_user,
|
||||
home);
|
||||
if (svc_len < 0 || (size_t)svc_len >= sizeof(service_content)) {
|
||||
fprintf(stderr, "%sFailed to build service file content.%s\n", ANSI_RED, ANSI_RESET);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (write_file_privileged_local(service_path, service_content, 0644U) != 0) {
|
||||
fprintf(stderr, "%sFailed to write service file at %s.%s\n", ANSI_RED, service_path, ANSI_RESET);
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* --- 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_privileged_command_local(reload_argv) != 0 ||
|
||||
run_privileged_command_local(enable_argv) != 0 ||
|
||||
run_privileged_command_local(start_argv) != 0) {
|
||||
fprintf(stderr, "%sFailed to enable/start systemd service %s.%s\n", ANSI_RED, service_name, ANSI_RESET);
|
||||
fprintf(stderr, " You can manually start with:\n");
|
||||
fprintf(stderr, " sudo systemctl daemon-reload\n");
|
||||
fprintf(stderr, " sudo systemctl enable %s\n", service_name);
|
||||
fprintf(stderr, " sudo systemctl start %s\n", service_name);
|
||||
return -1;
|
||||
}
|
||||
|
||||
fprintf(stderr, "\n%sSystem service installed and started.%s\n", ANSI_YELLOW, ANSI_RESET);
|
||||
fprintf(stderr, " Service: %s\n", service_name);
|
||||
fprintf(stderr, " User: %s\n", service_user);
|
||||
fprintf(stderr, " Home: %s\n", home);
|
||||
fprintf(stderr, " Binary: %s\n", bin_path);
|
||||
fprintf(stderr, " Nsec: embedded directly in ExecStart\n");
|
||||
fprintf(stderr, " Sudoers: /etc/sudoers.d/%s\n", service_user);
|
||||
fprintf(stderr, " Unit: %s\n", service_path);
|
||||
fprintf(stderr, "\n Useful commands:\n");
|
||||
fprintf(stderr, " sudo systemctl status %s\n", service_name);
|
||||
fprintf(stderr, " sudo journalctl -u %s -f\n", service_name);
|
||||
fprintf(stderr, " sudo systemctl restart %s\n", service_name);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static __attribute__((unused)) int maybe_prompt_write_path(char* path_out, size_t path_out_size) {
|
||||
if (!path_out || path_out_size == 0) return -1;
|
||||
char input[WIZARD_LINE_MAX];
|
||||
if (read_line_prompt("Path [./genesis.jsonc]: ", input, sizeof(input)) != 0) return -1;
|
||||
@@ -1298,7 +1608,7 @@ static int maybe_prompt_write_path(char* path_out, size_t path_out_size) {
|
||||
|
||||
static int new_agent_identity_step(didactyl_config_t* cfg) {
|
||||
for (;;) {
|
||||
render_wizard_page_header("Step 1 of 7", "New Agent Setup -- Identity");
|
||||
render_wizard_page_header("Step 2 of 7", "New Agent Setup -- Identity");
|
||||
print_option('g', "enerate a new Nostr keypair");
|
||||
print_option('p', "rovide an existing nsec");
|
||||
print_option('b', "ack");
|
||||
@@ -1359,6 +1669,16 @@ static int new_agent_identity_step(didactyl_config_t* cfg) {
|
||||
}
|
||||
|
||||
static int new_agent_flow(didactyl_config_t* cfg, char* genesis_path_out, size_t genesis_path_out_size) {
|
||||
(void)genesis_path_out;
|
||||
(void)genesis_path_out_size;
|
||||
char agent_name[OW_MAX_NAME_LEN] = {0};
|
||||
render_wizard_page_header("Step 1 of 7", "New Agent Setup -- Agent Profile");
|
||||
if (read_line_prompt(" Agent name [Didactyl]: ", agent_name, sizeof(agent_name)) != 0) return -1;
|
||||
if (line_is_quit(agent_name)) return -1;
|
||||
if (agent_name[0] == '\0') {
|
||||
snprintf(agent_name, sizeof(agent_name), "%s", "Didactyl");
|
||||
}
|
||||
|
||||
int rc = new_agent_identity_step(cfg);
|
||||
if (rc != 0) return rc < 0 ? -1 : 1;
|
||||
|
||||
@@ -1372,7 +1692,7 @@ static int new_agent_flow(didactyl_config_t* cfg, char* genesis_path_out, size_t
|
||||
ANSI_YELLOW,
|
||||
ANSI_RESET);
|
||||
} else {
|
||||
render_wizard_page_header("Step 3 of 7", "New Agent Setup -- Identity Validation");
|
||||
render_wizard_page_header("Step 4 of 7", "New Agent Setup -- Identity Validation");
|
||||
fprintf(stderr, " Agent kind 10002: %s\n", exists ? "FOUND (may overwrite existing state)" : "Agent npub available for new setup");
|
||||
if (admin_name_found) {
|
||||
fprintf(stderr, " Admin profile: %s\n", admin_name);
|
||||
@@ -1400,21 +1720,13 @@ static int new_agent_flow(didactyl_config_t* cfg, char* genesis_path_out, size_t
|
||||
if (prompt_llm_config(cfg) != 0) return -1;
|
||||
}
|
||||
|
||||
char agent_name[OW_MAX_NAME_LEN] = {0};
|
||||
render_wizard_page_header("Step 6 of 7", "New Agent Setup -- Agent Profile");
|
||||
if (read_line_prompt(" Agent name [Didactyl]: ", agent_name, sizeof(agent_name)) != 0) return -1;
|
||||
if (line_is_quit(agent_name)) return -1;
|
||||
if (agent_name[0] == '\0') {
|
||||
snprintf(agent_name, sizeof(agent_name), "%s", "Didactyl");
|
||||
}
|
||||
|
||||
if (sync_startup_kind0_event(cfg, agent_name) != 0) {
|
||||
fprintf(stderr, "%sFailed to prepare startup kind0 profile event.%s\n", ANSI_RED, ANSI_RESET);
|
||||
if (configure_default_skill_for_agent(cfg, agent_name) != 0) {
|
||||
fprintf(stderr, "%sFailed to prepare default skill event content.%s\n", ANSI_RED, ANSI_RESET);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (sync_startup_kind10002_event(cfg) != 0) {
|
||||
fprintf(stderr, "%sFailed to prepare startup kind10002 event.%s\n", ANSI_RED, ANSI_RESET);
|
||||
if (apply_default_startup_events(cfg, agent_name) != 0) {
|
||||
fprintf(stderr, "%sFailed to prepare startup events from defaults.%s\n", ANSI_RED, ANSI_RESET);
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -1427,14 +1739,12 @@ static int new_agent_flow(didactyl_config_t* cfg, char* genesis_path_out, size_t
|
||||
fprintf(stderr, " DM Protocol: nip04\n\n");
|
||||
|
||||
print_option('b', "oot the agent now");
|
||||
print_option('w', "rite genesis.jsonc (without nsec) and boot");
|
||||
print_option('i', "nclude nsec in genesis.jsonc and boot (security risk)");
|
||||
print_option('e', "xport genesis.jsonc (without nsec) and exit");
|
||||
print_option('i', "nstall dedicated-user systemd service and boot");
|
||||
print_option('s', "tart over");
|
||||
print_option('q', "uit");
|
||||
|
||||
wizard_option_t opts[] = {{'b', ""}, {'w', ""}, {'i', ""}, {'e', ""}, {'s', ""}, {'q', ""}};
|
||||
char c = read_menu_choice(opts, 6);
|
||||
wizard_option_t opts[] = {{'b', ""}, {'i', ""}, {'s', ""}, {'q', ""}};
|
||||
char c = read_menu_choice(opts, 4);
|
||||
if (c == 'q') return -1;
|
||||
if (c == 's') return 2;
|
||||
|
||||
@@ -1445,26 +1755,11 @@ static int new_agent_flow(didactyl_config_t* cfg, char* genesis_path_out, size_t
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (c == 'w' || c == 'i' || c == 'e') {
|
||||
char path[OW_MAX_URL_LEN] = {0};
|
||||
if (maybe_prompt_write_path(path, sizeof(path)) != 0) return -1;
|
||||
int include_nsec = (c == 'i') ? 1 : 0;
|
||||
if (include_nsec) {
|
||||
fprintf(stderr, "%sWarning: this writes nsec to disk in plaintext.%s\n", ANSI_YELLOW, ANSI_RESET);
|
||||
}
|
||||
if (write_genesis_file(cfg, path, include_nsec) != 0) {
|
||||
fprintf(stderr, "%sFailed to write genesis file: %s (%s)%s\n",
|
||||
ANSI_RED,
|
||||
path,
|
||||
strerror(errno),
|
||||
ANSI_RESET);
|
||||
if (c == 'i') {
|
||||
if (install_system_service_with_dedicated_user(cfg, agent_name) != 0) {
|
||||
fprintf(stderr, "%sFailed to install dedicated-user system service.%s\n", ANSI_RED, ANSI_RESET);
|
||||
return -1;
|
||||
}
|
||||
fprintf(stderr, "Wrote %s\n", path);
|
||||
if (genesis_path_out && genesis_path_out_size > 0) {
|
||||
snprintf(genesis_path_out, genesis_path_out_size, "%s", path);
|
||||
}
|
||||
if (c == 'e') return 1;
|
||||
fprintf(stderr, "%sStep 7 of 7 -- Booting new agent. Check your messages for initial greeting.%s\n",
|
||||
ANSI_YELLOW,
|
||||
ANSI_RESET);
|
||||
|
||||
@@ -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);
|
||||
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) {
|
||||
return execute_nostr_relay_info(args_json);
|
||||
}
|
||||
if (strcmp(tool_name, "nostr_subscription_status") == 0) {
|
||||
return execute_nostr_subscription_status(args_json);
|
||||
}
|
||||
if (strcmp(tool_name, "nostr_subscription_set") == 0) {
|
||||
return execute_nostr_subscription_set(args_json);
|
||||
}
|
||||
if (strcmp(tool_name, "nostr_encrypt") == 0) {
|
||||
return execute_nostr_encrypt(ctx, args_json);
|
||||
}
|
||||
@@ -81,6 +87,9 @@ char* tools_execute_legacy(tools_context_t* ctx, const char* tool_name, const ch
|
||||
if (strcmp(tool_name, "nostr_query") == 0) {
|
||||
return execute_nostr_query(args_json);
|
||||
}
|
||||
if (strcmp(tool_name, "nostr_my_events") == 0) {
|
||||
return execute_nostr_my_events(ctx, args_json);
|
||||
}
|
||||
if (strcmp(tool_name, "agent_version") == 0) {
|
||||
return execute_agent_version(args_json);
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ char* execute_model_get(const char* args_json);
|
||||
char* execute_model_set(tools_context_t* ctx, const char* args_json);
|
||||
char* execute_model_list(const char* args_json);
|
||||
char* execute_nostr_query(const char* args_json);
|
||||
char* execute_nostr_my_events(tools_context_t* ctx, const char* args_json);
|
||||
char* execute_nostr_pubkey(tools_context_t* ctx, const char* args_json);
|
||||
char* execute_nostr_npub(tools_context_t* ctx, const char* args_json);
|
||||
char* execute_nostr_encode(const char* args_json);
|
||||
@@ -30,6 +31,8 @@ char* execute_nostr_profile_get(const char* args_json);
|
||||
char* execute_nostr_nip05_lookup(const char* args_json);
|
||||
char* execute_nostr_relay_status(const char* args_json);
|
||||
char* execute_nostr_relay_info(const char* args_json);
|
||||
char* execute_nostr_subscription_status(const char* args_json);
|
||||
char* execute_nostr_subscription_set(const char* args_json);
|
||||
char* execute_nostr_dm_send(const char* args_json);
|
||||
char* execute_nostr_encrypt(tools_context_t* ctx, const char* args_json);
|
||||
char* execute_nostr_decrypt(tools_context_t* ctx, const char* args_json);
|
||||
|
||||
@@ -410,6 +410,54 @@ char* tools_build_openai_schema_json_legacy(const tools_context_t* ctx) {
|
||||
cJSON_AddItemToObject(t15, "function", t15_fn);
|
||||
cJSON_AddItemToArray(tools, t15);
|
||||
|
||||
cJSON* t15b = cJSON_CreateObject();
|
||||
cJSON* t15b_fn = cJSON_CreateObject();
|
||||
cJSON* t15b_params = cJSON_CreateObject();
|
||||
cJSON* t15b_props = cJSON_CreateObject();
|
||||
|
||||
cJSON_AddStringToObject(t15b, "type", "function");
|
||||
cJSON_AddStringToObject(t15b_fn, "name", "nostr_subscription_status");
|
||||
cJSON_AddStringToObject(t15b_fn, "description", "List currently managed runtime Nostr subscriptions and filters");
|
||||
cJSON_AddStringToObject(t15b_params, "type", "object");
|
||||
cJSON_AddItemToObject(t15b_params, "properties", t15b_props);
|
||||
cJSON_AddItemToObject(t15b_fn, "parameters", t15b_params);
|
||||
cJSON_AddItemToObject(t15b, "function", t15b_fn);
|
||||
cJSON_AddItemToArray(tools, t15b);
|
||||
|
||||
cJSON* t15c = cJSON_CreateObject();
|
||||
cJSON* t15c_fn = cJSON_CreateObject();
|
||||
cJSON* t15c_params = cJSON_CreateObject();
|
||||
cJSON* t15c_props = cJSON_CreateObject();
|
||||
cJSON* t15c_required = cJSON_CreateArray();
|
||||
|
||||
cJSON_AddStringToObject(t15c, "type", "function");
|
||||
cJSON_AddStringToObject(t15c_fn, "name", "nostr_subscription_set");
|
||||
cJSON_AddStringToObject(t15c_fn, "description", "Update one managed runtime subscription by name (toggle enabled and/or replace filter)");
|
||||
cJSON_AddStringToObject(t15c_params, "type", "object");
|
||||
cJSON_AddItemToObject(t15c_params, "properties", t15c_props);
|
||||
cJSON_AddItemToObject(t15c_params, "required", t15c_required);
|
||||
|
||||
cJSON* p_sub_name = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(p_sub_name, "type", "string");
|
||||
cJSON_AddStringToObject(p_sub_name, "description", "Subscription name (e.g. admin_context_profile, agent_context_notes, dms_kind4)");
|
||||
cJSON_AddItemToObject(t15c_props, "name", p_sub_name);
|
||||
|
||||
cJSON* p_sub_enabled = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(p_sub_enabled, "type", "boolean");
|
||||
cJSON_AddStringToObject(p_sub_enabled, "description", "Optional: enable or disable this subscription");
|
||||
cJSON_AddItemToObject(t15c_props, "enabled", p_sub_enabled);
|
||||
|
||||
cJSON* p_sub_filter = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(p_sub_filter, "type", "object");
|
||||
cJSON_AddStringToObject(p_sub_filter, "description", "Optional: replacement Nostr filter object");
|
||||
cJSON_AddItemToObject(t15c_props, "filter", p_sub_filter);
|
||||
|
||||
cJSON_AddItemToArray(t15c_required, cJSON_CreateString("name"));
|
||||
|
||||
cJSON_AddItemToObject(t15c_fn, "parameters", t15c_params);
|
||||
cJSON_AddItemToObject(t15c, "function", t15c_fn);
|
||||
cJSON_AddItemToArray(tools, t15c);
|
||||
|
||||
cJSON* t16 = cJSON_CreateObject();
|
||||
cJSON* t16_fn = cJSON_CreateObject();
|
||||
cJSON* t16_params = cJSON_CreateObject();
|
||||
@@ -1393,6 +1441,36 @@ char* tools_build_openai_schema_json_legacy(const tools_context_t* ctx) {
|
||||
cJSON_AddItemToObject(t49, "function", t49_fn);
|
||||
cJSON_AddItemToArray(tools, t49);
|
||||
|
||||
cJSON* t50 = cJSON_CreateObject();
|
||||
cJSON* t50_fn = cJSON_CreateObject();
|
||||
cJSON* t50_params = cJSON_CreateObject();
|
||||
cJSON* t50_props = cJSON_CreateObject();
|
||||
|
||||
cJSON_AddStringToObject(t50, "type", "function");
|
||||
cJSON_AddStringToObject(t50_fn, "name", "nostr_my_events");
|
||||
cJSON_AddStringToObject(t50_fn, "description", "Query recent events authored by this agent and return kind, event_id, timestamp, d_tag, and cache presence");
|
||||
cJSON_AddStringToObject(t50_params, "type", "object");
|
||||
cJSON_AddItemToObject(t50_params, "properties", t50_props);
|
||||
|
||||
cJSON* p_my_events_limit = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(p_my_events_limit, "type", "integer");
|
||||
cJSON_AddStringToObject(p_my_events_limit, "description", "Maximum number of authored events to query (1-2000, default 200)");
|
||||
cJSON_AddItemToObject(t50_props, "limit", p_my_events_limit);
|
||||
|
||||
cJSON* p_my_events_kind = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(p_my_events_kind, "type", "integer");
|
||||
cJSON_AddStringToObject(p_my_events_kind, "description", "Optional specific kind to query; omit for all kinds");
|
||||
cJSON_AddItemToObject(t50_props, "kind", p_my_events_kind);
|
||||
|
||||
cJSON* p_my_events_timeout = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(p_my_events_timeout, "type", "integer");
|
||||
cJSON_AddStringToObject(p_my_events_timeout, "description", "Query timeout in milliseconds (1000-60000, default 8000)");
|
||||
cJSON_AddItemToObject(t50_props, "timeout_ms", p_my_events_timeout);
|
||||
|
||||
cJSON_AddItemToObject(t50_fn, "parameters", t50_params);
|
||||
cJSON_AddItemToObject(t50, "function", t50_fn);
|
||||
cJSON_AddItemToArray(tools, t50);
|
||||
|
||||
char* out = cJSON_PrintUnformatted(tools);
|
||||
cJSON_Delete(tools);
|
||||
return out;
|
||||
|
||||
Reference in New Issue
Block a user