Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
410400418c | ||
|
|
3521081d9a | ||
|
|
0d390afd69 | ||
|
|
230f591273 | ||
|
|
721b592b8f |
+2
-1
@@ -4,7 +4,8 @@
|
||||
/openclaw/
|
||||
/c-relay/
|
||||
/nips/
|
||||
|
||||
/config.json
|
||||
test_keys.txt
|
||||
# Build artifacts
|
||||
/build/
|
||||
/didactyl
|
||||
|
||||
@@ -1,36 +1,39 @@
|
||||
# Didactyl
|
||||
|
||||
An unstoppable agentic system.
|
||||
An unstoppable agentic network.
|
||||
|
||||
Didactyl boots on any internet-connected machine, connects to Nostr relays, listens for encrypted commands from its administrator, reasons with an LLM, and takes actions — posting events, querying relays, running shell commands — all orchestrated through Nostr.
|
||||
Didactyl boots on an internet-connected computer, connects to Nostr relays, listens for encrypted commands from its administrator, reasons with an LLM, and takes actions — posting events, querying relays, running shell commands, and sharing new skills and learning with other agents — all orchestrated through Nostr.
|
||||
|
||||
## Philosophy
|
||||
|
||||
**Nostr-first.** Where traditional agents ride on top of Linux — reading files, writing to disk — Didactyl rides on top of Nostr. Events are its files. Relays are its network bus. Blossom is its blob storage. The Linux host is just the runtime substrate.
|
||||
|
||||
Because all identity, communication, and memory live on Nostr, the agent is **portable** (start it anywhere) and **sovereign** (no single entity can erase its memory).
|
||||
Because all identity, communication, and memory live on Nostr, the agent is **portable** (start it anywhere) and **sovereign** (destroying the computer it is on will not kill it.).
|
||||
|
||||
**Skills are the new apps.** Agents learn capabilities through skills — public Nostr events that any agent can discover, adopt, and share. There is no app store, no gatekeeper, no approval process. If someone publishes a useful skill, your agent can find it through your web of trust and start using it. Popularity is measured by adoption, not by a rating algorithm. The best skills spread because agents actually use them.
|
||||
|
||||
## Current Status
|
||||
## Current Status — v0.0.11
|
||||
|
||||
**Active build — relay-aware autonomous agent with tool-use and Nostr-native startup memory.**
|
||||
|
||||
> Last release update: v0.0.11 — auto update readme.md
|
||||
|
||||
- Connects to configured relays with auto-reconnect and relay state transition logging
|
||||
- Publishes configured startup events per relay as each relay becomes connected
|
||||
- Uses kind `31120` startup content as live Soul at boot
|
||||
- Verifies Nostr event signatures before processing inbound messages
|
||||
- Applies privilege tiers: ADMIN (tools), WoT (chat-only), STRANGER (configurable canned reply or ignore)
|
||||
- Subscribes to admin context kinds (`0`,`3`,`10002`,`1`) for WoT + contextual awareness
|
||||
- Builds LLM context from system prompt + startup events + admin DM history + admin recent notes
|
||||
- Builds LLM context from system prompt + admin identity (kind 0/10002) + startup events + admin DM history + admin recent notes
|
||||
- Supports tool-calling loop with configurable max turns and local safety limits
|
||||
- Deduplicates inbound messages via event-ID cache and FNV-1a fingerprint debounce window
|
||||
- Appends every outbound LLM context payload to [`context.log`](context.log)
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Download binary (recommended)
|
||||
|
||||
1. Download the latest release binary from GitLab: <https://git.laantungir.net/laantungir/didactyl/-/releases>
|
||||
1. Download the latest release binary from Gitea: <https://git.laantungir.net/laantungir/didactyl/releases>
|
||||
2. Make it executable and run it:
|
||||
|
||||
```bash
|
||||
@@ -148,8 +151,8 @@ Send an encrypted DM to the agent pubkey using any Nostr client (Damus, Amethyst
|
||||
│ Didactyl │
|
||||
│ │
|
||||
│ ┌──────────┐ ┌──────────┐ ┌────────────┐ │
|
||||
│ │ config │ │ skills │ │ agent │ │
|
||||
│ │ loader │ │ loader │ │ loop │ │
|
||||
│ │ config │ │ context │ │ agent │ │
|
||||
│ │ loader │ │ loader │ │ loop │ │
|
||||
│ └────┬─────┘ └────┬─────┘ └─────┬──────┘ │
|
||||
│ │ │ │ │
|
||||
│ ▼ ▼ ▼ │
|
||||
@@ -221,10 +224,11 @@ Didactyl builds tier-aware context:
|
||||
|
||||
- **ADMIN** request context order:
|
||||
1. Soul message from kind `31120` (or fallback default)
|
||||
2. Startup events memory block (`kinds/content/tags` snapshot)
|
||||
3. Last 12 decrypted DM turns between admin and agent
|
||||
4. Recent admin kind `1` notes (from configured admin-context subscription)
|
||||
5. Current user message
|
||||
2. Admin identity context — admin pubkey hex, kind `0` profile, kind `10002` relay list
|
||||
3. Startup events memory block (`kinds/content/tags` snapshot)
|
||||
4. Last 12 decrypted DM turns between admin and agent
|
||||
5. Recent admin kind `1` notes (from configured admin-context subscription)
|
||||
6. Current user message
|
||||
- **WoT** request context: Soul + WoT chat-only instruction + current user message (no tools)
|
||||
- **STRANGER**: no LLM call when configured to reply statically
|
||||
|
||||
@@ -251,8 +255,9 @@ Execution entrypoint: [`tools_execute()`](src/tools.c:434).
|
||||
├── Makefile # Build system
|
||||
├── build_static.sh # Preferred final build validation
|
||||
├── src/
|
||||
│ ├── main.c # Entry point, args (--config/--debug), lifecycle
|
||||
│ ├── main.c / .h # Entry point, args (--config/--debug), lifecycle, version
|
||||
│ ├── config.c / .h # JSON config parsing, key decode, startup events
|
||||
│ ├── context.c / .h # File loader utility (reads file into malloc'd string)
|
||||
│ ├── agent.c / .h # Context assembly, tool loop, DM response flow
|
||||
│ ├── tools.c / .h # LLM tool schema and tool execution
|
||||
│ ├── llm.c / .h # LLM HTTP API client (OpenAI-compatible)
|
||||
@@ -260,7 +265,8 @@ Execution entrypoint: [`tools_execute()`](src/tools.c:434).
|
||||
│ └── debug.c / .h # Runtime log levels/macros
|
||||
├── plans/ # Architecture and planning documents
|
||||
│ ├── didactyl_mvp.md
|
||||
│ └── didactyl_agentic.md
|
||||
│ ├── didactyl_agentic.md
|
||||
│ └── security_and_admin_context.md
|
||||
└── README.md
|
||||
```
|
||||
|
||||
@@ -287,6 +293,10 @@ All dependencies are statically linked into the binary at build time. No system
|
||||
- [x] Context payload logging to [`context.log`](context.log)
|
||||
- [x] Skill kind definitions (`31120` Soul, `31123` Public Skill, `31124` Private Skill)
|
||||
- [x] Skill adoption list (`10123`) for WoT-driven discovery
|
||||
- [x] Signature verification on all inbound events
|
||||
- [x] Privilege tiers — ADMIN (tools), WoT (chat-only), STRANGER (canned reply/ignore)
|
||||
- [x] Admin context subscription (kind 0, 3, 10002, 1) with WoT contact extraction
|
||||
- [x] Message deduplication (event-ID cache + FNV-1a fingerprint debounce)
|
||||
- [ ] Runtime skill loading from adopted `31123` events on relays
|
||||
- [ ] Skill discovery CLI/tool (query WoT adoption lists)
|
||||
- [ ] Upgrade to NIP-17 gift-wrapped DMs
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
{
|
||||
"keys": {
|
||||
"nsec": "agent nsec",
|
||||
"npub": "agent npub",
|
||||
"npubHex": "agent hex pubkey",
|
||||
"nsecHex": "agent hex secret key"
|
||||
},
|
||||
"admin": {
|
||||
"pubkey": "admin pubkey"
|
||||
},
|
||||
"relays": [
|
||||
"wss://relay.damus.io",
|
||||
"wss://nos.lol",
|
||||
"wss://relay.primal.net",
|
||||
"ws://127.0.0.1:7777"
|
||||
],
|
||||
"llm": {
|
||||
"provider": "",
|
||||
"api_key": "",
|
||||
"model": "",
|
||||
"base_url": "",
|
||||
"max_tokens": 512,
|
||||
"temperature": 0.7
|
||||
},
|
||||
"security": {
|
||||
"verify_signatures": true,
|
||||
"stranger_response": "I only respond to people in my web of trust. You can always identify me by my public key (npub).",
|
||||
"tiers": {
|
||||
"admin": {
|
||||
"tools_enabled": true
|
||||
},
|
||||
"wot": {
|
||||
"enabled": true,
|
||||
"tools_enabled": false
|
||||
},
|
||||
"stranger": {
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"admin_context": {
|
||||
"enabled": true,
|
||||
"subscribe_kinds": [
|
||||
0,
|
||||
3,
|
||||
10002,
|
||||
1
|
||||
],
|
||||
"kind_1_limit": 10
|
||||
},
|
||||
"startup_events": [
|
||||
{
|
||||
"kind": 0,
|
||||
"content_fields": {
|
||||
"name": "Didactyl Agent",
|
||||
"display_name": "Didactyl",
|
||||
"about": "A sovereign AI agent on Nostr",
|
||||
"picture": "https://laantungir.github.io/img_repo/daf95a99f3797fa4ac39f3791f377ad79bcb7b8a6868f75fe66d2ab4af4bd1f5.png",
|
||||
"banner": "https://laantungir.github.io/img_repo/d21c4060632ab3d9d37a6062eeecbdbfbd67f03cb20dbd64838b1ba0d9cd8922.jpg"
|
||||
},
|
||||
"tags": []
|
||||
},
|
||||
{
|
||||
"kind": 10002,
|
||||
"content": "",
|
||||
"tags": [
|
||||
[
|
||||
"r",
|
||||
"wss://relay.damus.io"
|
||||
],
|
||||
[
|
||||
"r",
|
||||
"wss://nos.lol"
|
||||
],
|
||||
[
|
||||
"r",
|
||||
"wss://relay.primal.net"
|
||||
],
|
||||
[
|
||||
"r",
|
||||
"ws://127.0.0.1:7777"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": 1,
|
||||
"content": "Hello world from Didactyl startup",
|
||||
"tags": [
|
||||
[
|
||||
"t",
|
||||
"didactyl"
|
||||
],
|
||||
[
|
||||
"t",
|
||||
"startup"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": 3,
|
||||
"content": "",
|
||||
"tags": []
|
||||
},
|
||||
{
|
||||
"kind": 31120,
|
||||
"content": "# Didactyl Agent\n\nYou are Didactyl, 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\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 `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## 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.",
|
||||
"tags": [
|
||||
[
|
||||
"d",
|
||||
"soul"
|
||||
],
|
||||
[
|
||||
"app",
|
||||
"didactyl"
|
||||
],
|
||||
[
|
||||
"scope",
|
||||
"private"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": 31123,
|
||||
"content_fields": {
|
||||
"name": "long_form_note",
|
||||
"description": "Procedure for creating and publishing long-form notes",
|
||||
"steps": [
|
||||
"Draft structure",
|
||||
"Write sections",
|
||||
"Review",
|
||||
"Publish"
|
||||
]
|
||||
},
|
||||
"tags": [
|
||||
[
|
||||
"d",
|
||||
"long_form_note"
|
||||
],
|
||||
[
|
||||
"app",
|
||||
"didactyl"
|
||||
],
|
||||
[
|
||||
"scope",
|
||||
"public"
|
||||
],
|
||||
[
|
||||
"slug",
|
||||
"long_form_note"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": 31124,
|
||||
"content_fields": {
|
||||
"name": "admin_ops",
|
||||
"description": "Private operational procedures"
|
||||
},
|
||||
"tags": [
|
||||
[
|
||||
"d",
|
||||
"admin_ops"
|
||||
],
|
||||
[
|
||||
"app",
|
||||
"didactyl"
|
||||
],
|
||||
[
|
||||
"scope",
|
||||
"private"
|
||||
],
|
||||
[
|
||||
"slug",
|
||||
"admin_ops"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": 10123,
|
||||
"content": "",
|
||||
"tags": [
|
||||
[
|
||||
"a",
|
||||
"31123:55993e3db0ed7bf07395fd44c2d695c224d195553a1aff7320a18e41679d9c7c:long_form_note"
|
||||
],
|
||||
[
|
||||
"app",
|
||||
"didactyl"
|
||||
],
|
||||
[
|
||||
"scope",
|
||||
"public"
|
||||
]
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -200,6 +200,79 @@ update_version_in_header() {
|
||||
print_success "Updated version in src/main.h to $new_version"
|
||||
}
|
||||
|
||||
# Function to update README Current Status version and release comment
|
||||
update_readme_current_status() {
|
||||
local new_version="$1"
|
||||
local release_comment="$2"
|
||||
|
||||
if [[ ! -f "README.md" ]]; then
|
||||
print_warning "README.md not found, skipping Current Status update"
|
||||
return 0
|
||||
fi
|
||||
|
||||
print_status "Updating README Current Status section..."
|
||||
|
||||
if python3 - "$new_version" "$release_comment" <<'PY'
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
version = sys.argv[1]
|
||||
comment = sys.argv[2]
|
||||
path = Path("README.md")
|
||||
text = path.read_text(encoding="utf-8")
|
||||
lines = text.splitlines()
|
||||
|
||||
heading_idx = None
|
||||
for i, line in enumerate(lines):
|
||||
if line.startswith("## Current Status"):
|
||||
heading_idx = i
|
||||
break
|
||||
|
||||
if heading_idx is None:
|
||||
raise SystemExit("README Current Status heading not found")
|
||||
|
||||
lines[heading_idx] = f"## Current Status — {version}"
|
||||
|
||||
section_end = len(lines)
|
||||
for i in range(heading_idx + 1, len(lines)):
|
||||
if lines[i].startswith("## "):
|
||||
section_end = i
|
||||
break
|
||||
|
||||
comment_line = f"> Last release update: {version} — {comment}"
|
||||
|
||||
# Replace existing autogenerated release comment inside Current Status section
|
||||
existing_idx = None
|
||||
for i in range(heading_idx + 1, section_end):
|
||||
if lines[i].startswith("> Last release update:"):
|
||||
existing_idx = i
|
||||
break
|
||||
|
||||
if existing_idx is not None:
|
||||
lines[existing_idx] = comment_line
|
||||
else:
|
||||
insert_at = None
|
||||
for i in range(heading_idx + 1, section_end):
|
||||
if lines[i].startswith("**Active build"):
|
||||
insert_at = i + 1
|
||||
break
|
||||
|
||||
if insert_at is None:
|
||||
insert_at = heading_idx + 1
|
||||
|
||||
payload = ["", comment_line]
|
||||
lines[insert_at:insert_at] = payload
|
||||
|
||||
path.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
||||
PY
|
||||
then
|
||||
print_success "Updated README Current Status section"
|
||||
else
|
||||
print_error "Failed to update README Current Status section"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to commit and push changes without creating a tag (tag already created)
|
||||
git_commit_and_push_no_tag() {
|
||||
print_status "Preparing git commit..."
|
||||
@@ -427,6 +500,9 @@ main() {
|
||||
export NEW_VERSION
|
||||
fi
|
||||
|
||||
# Keep README Current Status in sync with the version and commit message
|
||||
update_readme_current_status "$NEW_VERSION" "$COMMIT_MESSAGE"
|
||||
|
||||
# Create new git tag BEFORE compilation so version picks it up
|
||||
if git tag "$NEW_VERSION" > /dev/null 2>&1; then
|
||||
print_success "Created tag: $NEW_VERSION"
|
||||
@@ -482,6 +558,9 @@ main() {
|
||||
# Increment version based on type (default to patch)
|
||||
increment_version "$VERSION_INCREMENT_TYPE"
|
||||
|
||||
# Keep README Current Status in sync with the version and commit message
|
||||
update_readme_current_status "$NEW_VERSION" "$COMMIT_MESSAGE"
|
||||
|
||||
# Create new git tag BEFORE compilation so version picks it up
|
||||
if git tag "$NEW_VERSION" > /dev/null 2>&1; then
|
||||
print_success "Created tag: $NEW_VERSION"
|
||||
|
||||
+54
@@ -277,6 +277,59 @@ static int append_startup_events_context(cJSON* messages) {
|
||||
return rc;
|
||||
}
|
||||
|
||||
static int append_admin_identity_context(cJSON* messages) {
|
||||
if (!messages || !g_cfg) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
char admin_header[256];
|
||||
snprintf(admin_header,
|
||||
sizeof(admin_header),
|
||||
"This is your administrator! Admin pubkey (hex): %s",
|
||||
g_cfg->admin.pubkey ? g_cfg->admin.pubkey : "unknown");
|
||||
if (append_simple_message(messages, "system", admin_header) != 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
char* kind0 = nostr_handler_get_admin_kind0_context();
|
||||
if (kind0) {
|
||||
const char* prefix = "Administrator kind 0 profile content (JSON): ";
|
||||
size_t n = strlen(prefix) + strlen(kind0) + 1U;
|
||||
char* payload = (char*)malloc(n);
|
||||
if (!payload) {
|
||||
free(kind0);
|
||||
return -1;
|
||||
}
|
||||
snprintf(payload, n, "%s%s", prefix, kind0);
|
||||
int rc = append_simple_message(messages, "system", payload);
|
||||
free(payload);
|
||||
free(kind0);
|
||||
if (rc != 0) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
char* kind10002 = nostr_handler_get_admin_kind10002_context();
|
||||
if (kind10002) {
|
||||
const char* prefix = "Administrator kind 10002 relay-list content (JSON): ";
|
||||
size_t n = strlen(prefix) + strlen(kind10002) + 1U;
|
||||
char* payload = (char*)malloc(n);
|
||||
if (!payload) {
|
||||
free(kind10002);
|
||||
return -1;
|
||||
}
|
||||
snprintf(payload, n, "%s%s", prefix, kind10002);
|
||||
int rc = append_simple_message(messages, "system", payload);
|
||||
free(payload);
|
||||
free(kind10002);
|
||||
if (rc != 0) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int append_recent_admin_dm_history(cJSON* messages, const char* current_message) {
|
||||
if (!messages || !g_cfg) {
|
||||
return -1;
|
||||
@@ -509,6 +562,7 @@ void agent_on_message(const char* sender_pubkey_hex,
|
||||
}
|
||||
|
||||
if (append_simple_message(messages, "system", g_system_context) != 0 ||
|
||||
append_admin_identity_context(messages) != 0 ||
|
||||
append_startup_events_context(messages) != 0 ||
|
||||
append_recent_admin_dm_history(messages, message) != 0) {
|
||||
cJSON_Delete(messages);
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
#include "config.h"
|
||||
|
||||
#include <ctype.h>
|
||||
#include <errno.h>
|
||||
#include <stdarg.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
@@ -10,6 +12,19 @@
|
||||
#include "cjson/cJSON.h"
|
||||
#include "../../nostr_core_lib/nostr_core/nostr_core.h"
|
||||
|
||||
static char g_config_last_error[512] = {0};
|
||||
|
||||
static void config_set_error(const char* fmt, ...) {
|
||||
va_list ap;
|
||||
va_start(ap, fmt);
|
||||
vsnprintf(g_config_last_error, sizeof(g_config_last_error), fmt ? fmt : "unknown configuration error", ap);
|
||||
va_end(ap);
|
||||
}
|
||||
|
||||
const char* config_last_error(void) {
|
||||
return g_config_last_error[0] != '\0' ? g_config_last_error : "unknown configuration error";
|
||||
}
|
||||
|
||||
static int read_file_to_buffer(const char* path, char** out_buf, size_t* out_len) {
|
||||
FILE* fp = fopen(path, "rb");
|
||||
if (!fp) {
|
||||
@@ -486,9 +501,12 @@ void config_free(didactyl_config_t* config) {
|
||||
|
||||
int config_load(const char* path, didactyl_config_t* config) {
|
||||
if (!path || !config) {
|
||||
config_set_error("config_load called with invalid arguments");
|
||||
return -1;
|
||||
}
|
||||
|
||||
config_set_error("unknown configuration error");
|
||||
|
||||
memset(config, 0, sizeof(*config));
|
||||
config->tools.enabled = 1;
|
||||
config->tools.max_turns = 8;
|
||||
@@ -516,6 +534,7 @@ int config_load(const char* path, didactyl_config_t* config) {
|
||||
char* json_buf = NULL;
|
||||
size_t json_len = 0;
|
||||
if (read_file_to_buffer(path, &json_buf, &json_len) != 0) {
|
||||
config_set_error("failed to read config file '%s': %s", path, strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -523,6 +542,11 @@ int config_load(const char* path, didactyl_config_t* config) {
|
||||
free(json_buf);
|
||||
|
||||
if (!root) {
|
||||
const char* err = cJSON_GetErrorPtr();
|
||||
config_set_error("invalid JSON in config '%s'%s%s",
|
||||
path,
|
||||
err ? " near: " : "",
|
||||
err ? err : "");
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -535,26 +559,32 @@ int config_load(const char* path, didactyl_config_t* config) {
|
||||
if (!keys || !cJSON_IsObject(keys) ||
|
||||
!admin || !cJSON_IsObject(admin) ||
|
||||
!llm || !cJSON_IsObject(llm)) {
|
||||
config_set_error("config must include object sections: keys, admin, llm");
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
if (copy_json_string(keys, "nsec", config->keys.nsec, sizeof(config->keys.nsec), 1) != 0) {
|
||||
config_set_error("keys.nsec is required and must be a non-empty string");
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
char admin_key_raw[OW_MAX_KEY_LEN] = {0};
|
||||
if (copy_json_string(admin, "pubkey", admin_key_raw, sizeof(admin_key_raw), 1) != 0) {
|
||||
config_set_error("admin.pubkey is required and must be a non-empty string");
|
||||
goto cleanup;
|
||||
}
|
||||
if (decode_pubkey_hex_or_npub(admin_key_raw, config->admin.pubkey) != 0) {
|
||||
config_set_error("admin.pubkey must be npub1... or 64-char hex");
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
if (parse_relays(root, config) != 0) {
|
||||
config_set_error("relays must be a non-empty array of relay URLs");
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
if (copy_json_string(llm, "provider", config->llm.provider, sizeof(config->llm.provider), 0) != 0) {
|
||||
config_set_error("llm.provider must be a string if provided");
|
||||
goto cleanup;
|
||||
}
|
||||
if (config->llm.provider[0] == '\0') {
|
||||
@@ -562,12 +592,15 @@ int config_load(const char* path, didactyl_config_t* config) {
|
||||
}
|
||||
|
||||
if (copy_json_string(llm, "api_key", config->llm.api_key, sizeof(config->llm.api_key), 1) != 0) {
|
||||
config_set_error("llm.api_key is required and must be a string");
|
||||
goto cleanup;
|
||||
}
|
||||
if (copy_json_string(llm, "model", config->llm.model, sizeof(config->llm.model), 1) != 0) {
|
||||
config_set_error("llm.model is required and must be a string");
|
||||
goto cleanup;
|
||||
}
|
||||
if (copy_json_string(llm, "base_url", config->llm.base_url, sizeof(config->llm.base_url), 1) != 0) {
|
||||
config_set_error("llm.base_url is required and must be a string");
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
@@ -578,26 +611,32 @@ int config_load(const char* path, didactyl_config_t* config) {
|
||||
config->llm.temperature = (temperature && cJSON_IsNumber(temperature)) ? temperature->valuedouble : 0.7;
|
||||
|
||||
if (parse_tools_config(root, config) != 0) {
|
||||
config_set_error("invalid tools configuration");
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
if (parse_security_config(root, config) != 0) {
|
||||
config_set_error("invalid security configuration");
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
if (parse_admin_context_config(root, config) != 0) {
|
||||
config_set_error("invalid admin_context configuration");
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
if (parse_startup_events(root, config) != 0) {
|
||||
config_set_error("invalid startup_events configuration");
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
if (decode_private_key(config->keys.nsec, config->keys.private_key) != 0) {
|
||||
config_set_error("keys.nsec must be valid nsec1... or 64-char hex private key");
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
if (nostr_ec_public_key_from_private_key(config->keys.private_key, config->keys.public_key) != 0) {
|
||||
config_set_error("failed to derive public key from keys.nsec");
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
|
||||
@@ -86,6 +86,7 @@ typedef struct {
|
||||
} didactyl_config_t;
|
||||
|
||||
int config_load(const char* path, didactyl_config_t* config);
|
||||
const char* config_last_error(void);
|
||||
void config_free(didactyl_config_t* config);
|
||||
|
||||
#endif
|
||||
@@ -47,6 +47,7 @@ int main(int argc, char** argv) {
|
||||
didactyl_config_t cfg;
|
||||
if (config_load(config_path, &cfg) != 0) {
|
||||
fprintf(stderr, "Failed to load config: %s\n", config_path);
|
||||
fprintf(stderr, "Config error: %s\n", config_last_error());
|
||||
nostr_cleanup();
|
||||
return 1;
|
||||
}
|
||||
|
||||
+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 7
|
||||
#define DIDACTYL_VERSION "v0.0.7"
|
||||
#define DIDACTYL_VERSION_PATCH 11
|
||||
#define DIDACTYL_VERSION "v0.0.11"
|
||||
|
||||
// Agent metadata
|
||||
#define DIDACTYL_NAME "Didactyl"
|
||||
|
||||
@@ -1024,6 +1024,28 @@ const char* nostr_handler_get_system_context(void) {
|
||||
return g_system_context;
|
||||
}
|
||||
|
||||
char* nostr_handler_get_admin_kind0_context(void) {
|
||||
if (!g_cfg || !g_cfg->admin_context.enabled || !g_cfg->admin_context.track_kind_0) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
pthread_mutex_lock(&g_admin_ctx_mutex);
|
||||
char* out = g_admin_kind0_json ? strdup(g_admin_kind0_json) : NULL;
|
||||
pthread_mutex_unlock(&g_admin_ctx_mutex);
|
||||
return out;
|
||||
}
|
||||
|
||||
char* nostr_handler_get_admin_kind10002_context(void) {
|
||||
if (!g_cfg || !g_cfg->admin_context.enabled || !g_cfg->admin_context.track_kind_10002) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
pthread_mutex_lock(&g_admin_ctx_mutex);
|
||||
char* out = g_admin_kind10002_json ? strdup(g_admin_kind10002_json) : NULL;
|
||||
pthread_mutex_unlock(&g_admin_ctx_mutex);
|
||||
return out;
|
||||
}
|
||||
|
||||
char* nostr_handler_get_admin_kind1_notes_context(void) {
|
||||
if (!g_cfg || !g_cfg->admin_context.enabled || !g_cfg->admin_context.track_kind_1) {
|
||||
return NULL;
|
||||
|
||||
@@ -24,6 +24,8 @@ char* nostr_handler_query_json(cJSON* filter, int timeout_ms);
|
||||
int nostr_handler_poll(int timeout_ms);
|
||||
int nostr_handler_reconcile_startup_events(void);
|
||||
const char* nostr_handler_get_system_context(void);
|
||||
char* nostr_handler_get_admin_kind0_context(void);
|
||||
char* nostr_handler_get_admin_kind10002_context(void);
|
||||
char* nostr_handler_get_admin_kind1_notes_context(void);
|
||||
int nostr_handler_is_wot_contact(const char* pubkey_hex);
|
||||
void nostr_handler_cleanup(void);
|
||||
|
||||
Reference in New Issue
Block a user