Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9505ee16bf | ||
|
|
d49e4ec4cb | ||
|
|
47bb5d76bc | ||
|
|
90948989ac | ||
|
|
b1a767f9b5 | ||
|
|
650df7bc41 | ||
|
|
9d06f18769 | ||
|
|
527016c078 | ||
|
|
c221104bc0 | ||
|
|
253aa4b5fc | ||
|
|
82362e4f3f | ||
|
|
7675109afc |
+1
-1
@@ -10,7 +10,7 @@ test_keys.txt
|
||||
|
||||
/mongoose/
|
||||
/Trash/
|
||||
|
||||
deploy_lt.sh
|
||||
|
||||
# Build artifacts
|
||||
/build/
|
||||
|
||||
+58
-37
@@ -8,41 +8,58 @@ FROM alpine:3.19 AS builder
|
||||
# Re-declare build argument in this stage
|
||||
ARG DEBUG_BUILD=false
|
||||
|
||||
# Install build dependencies
|
||||
RUN apk add --no-cache \
|
||||
build-base \
|
||||
musl-dev \
|
||||
git \
|
||||
cmake \
|
||||
pkgconfig \
|
||||
autoconf \
|
||||
automake \
|
||||
libtool \
|
||||
openssl-dev \
|
||||
openssl-libs-static \
|
||||
zlib-dev \
|
||||
zlib-static \
|
||||
curl-dev \
|
||||
curl-static \
|
||||
nghttp2-dev \
|
||||
nghttp2-static \
|
||||
c-ares-dev \
|
||||
c-ares-static \
|
||||
libpsl-dev \
|
||||
libpsl-static \
|
||||
libidn2-dev \
|
||||
libidn2-static \
|
||||
libunistring-dev \
|
||||
libunistring-static \
|
||||
brotli-dev \
|
||||
brotli-static \
|
||||
zstd-dev \
|
||||
zstd-static \
|
||||
sqlite-dev \
|
||||
sqlite-static \
|
||||
linux-headers \
|
||||
wget \
|
||||
bash
|
||||
# Install build dependencies (with retry logic for transient mirror/network failures)
|
||||
RUN set -eux; \
|
||||
printf '%s\n' \
|
||||
'https://dl-cdn.alpinelinux.org/alpine/v3.19/main' \
|
||||
'https://dl-cdn.alpinelinux.org/alpine/v3.19/community' \
|
||||
> /etc/apk/repositories; \
|
||||
APK_OK=0; \
|
||||
for attempt in 1 2 3 4 5; do \
|
||||
if apk update && apk add --no-cache \
|
||||
build-base \
|
||||
musl-dev \
|
||||
git \
|
||||
cmake \
|
||||
pkgconfig \
|
||||
autoconf \
|
||||
automake \
|
||||
libtool \
|
||||
openssl-dev \
|
||||
openssl-libs-static \
|
||||
zlib-dev \
|
||||
zlib-static \
|
||||
curl-dev \
|
||||
curl-static \
|
||||
nghttp2-dev \
|
||||
nghttp2-static \
|
||||
c-ares-dev \
|
||||
c-ares-static \
|
||||
libpsl-dev \
|
||||
libpsl-static \
|
||||
libidn2-dev \
|
||||
libidn2-static \
|
||||
libunistring-dev \
|
||||
libunistring-static \
|
||||
brotli-dev \
|
||||
brotli-static \
|
||||
zstd-dev \
|
||||
zstd-static \
|
||||
sqlite-dev \
|
||||
sqlite-static \
|
||||
linux-headers \
|
||||
wget \
|
||||
bash; then \
|
||||
APK_OK=1; \
|
||||
break; \
|
||||
fi; \
|
||||
echo "apk dependency install failed (attempt ${attempt}/5), retrying..."; \
|
||||
sleep $((attempt * 2)); \
|
||||
done; \
|
||||
if [ "$APK_OK" -ne 1 ]; then \
|
||||
echo "ERROR: failed to install Alpine dependencies after 5 attempts"; \
|
||||
exit 33; \
|
||||
fi
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /build
|
||||
@@ -68,6 +85,7 @@ COPY nostr_core_lib /build/nostr_core_lib/
|
||||
RUN cd nostr_core_lib && \
|
||||
chmod +x build.sh && \
|
||||
sed -i 's/CFLAGS="-Wall -Wextra -std=c99 -fPIC -O2"/CFLAGS="-U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=0 -Wall -Wextra -std=c99 -fPIC -O2"/' build.sh && \
|
||||
sed -i 's/CC="aarch64-linux-gnu-gcc"/CC="gcc"/' build.sh && \
|
||||
rm -f *.o *.a 2>/dev/null || true && \
|
||||
./build.sh --nips=all
|
||||
|
||||
@@ -78,7 +96,10 @@ COPY Makefile /build/Makefile
|
||||
# Build didactyl with full static linking (only rebuilds when src/ changes)
|
||||
# Disable fortification to avoid __*_chk symbols that don't exist in MUSL
|
||||
# Use conditional compilation flags based on DEBUG_BUILD argument
|
||||
RUN if [ "$DEBUG_BUILD" = "true" ]; then \
|
||||
RUN NOSTR_LIB=$(ls /build/nostr_core_lib/libnostr_core_*.a 2>/dev/null | head -1) && \
|
||||
if [ -z "$NOSTR_LIB" ]; then echo "ERROR: nostr_core_lib .a not found"; exit 1; fi && \
|
||||
echo "Using nostr library: $NOSTR_LIB" && \
|
||||
if [ "$DEBUG_BUILD" = "true" ]; then \
|
||||
CFLAGS="-g -O2 -DDEBUG"; \
|
||||
STRIP_CMD="echo 'Keeping debug symbols'"; \
|
||||
echo "Building with DEBUG symbols enabled (optimized with -O2)"; \
|
||||
@@ -103,7 +124,7 @@ RUN if [ "$DEBUG_BUILD" = "true" ]; then \
|
||||
src/tools/tool_skill.c src/tools/tool_nostr_post.c src/tools/tool_memory.c src/tools/tool_config.c src/trigger_manager.c \
|
||||
src/prompt_template.c src/http_api.c src/setup_wizard.c src/mongoose.c src/debug.c \
|
||||
-o /build/didactyl_static \
|
||||
nostr_core_lib/libnostr_core_x64.a \
|
||||
$NOSTR_LIB \
|
||||
-lsecp256k1 \
|
||||
$OPENSSL_LIBS \
|
||||
$CURL_LIBS \
|
||||
|
||||
@@ -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.78
|
||||
## Current Status — v0.1.0
|
||||
|
||||
**Active build — this project is barely working. Experiment at your own risk.**
|
||||
|
||||
> Last release update: v0.0.78 — Seed default skill into self-skill cache during startup reconcile
|
||||
> Last release update: v0.1.0 — Release dual-architecture static binaries (x86_64 + arm64)
|
||||
|
||||
- Connects to configured relays with auto-reconnect and relay state transition logging
|
||||
- Publishes configured startup events per relay as each relay becomes connected
|
||||
|
||||
+106
-20
@@ -11,8 +11,44 @@ DOCKERFILE="$SCRIPT_DIR/Dockerfile.alpine-musl"
|
||||
|
||||
# Parse command line arguments
|
||||
DEBUG_BUILD=false
|
||||
if [[ "$1" == "--debug" ]]; then
|
||||
DEBUG_BUILD=true
|
||||
TARGET_PLATFORM=""
|
||||
ALL_PLATFORMS=false
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--debug)
|
||||
DEBUG_BUILD=true
|
||||
;;
|
||||
--platform=*)
|
||||
TARGET_PLATFORM="${arg#*=}"
|
||||
;;
|
||||
--all-platforms|--all_platforms|-a)
|
||||
ALL_PLATFORMS=true
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ "$ALL_PLATFORMS" = true ] && [ -n "$TARGET_PLATFORM" ]; then
|
||||
echo "ERROR: --all-platforms cannot be used together with --platform"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$ALL_PLATFORMS" = true ]; then
|
||||
echo "Building all supported platforms (linux/amd64 + linux/arm64)..."
|
||||
echo ""
|
||||
DEBUG_FLAG=""
|
||||
if [ "$DEBUG_BUILD" = true ]; then
|
||||
DEBUG_FLAG="--debug"
|
||||
fi
|
||||
|
||||
"$SCRIPT_DIR/build_static.sh" $DEBUG_FLAG --platform=linux/amd64
|
||||
"$SCRIPT_DIR/build_static.sh" $DEBUG_FLAG --platform=linux/arm64
|
||||
|
||||
echo ""
|
||||
echo "✓ Multi-platform build complete"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "$DEBUG_BUILD" = true ]; then
|
||||
echo "=========================================="
|
||||
echo "Didactyl MUSL Static Binary Builder (DEBUG MODE)"
|
||||
echo "=========================================="
|
||||
@@ -61,24 +97,45 @@ DOCKER_CMD="docker"
|
||||
echo "✓ Docker is available and running"
|
||||
echo ""
|
||||
|
||||
# Detect architecture
|
||||
ARCH=$(uname -m)
|
||||
case "$ARCH" in
|
||||
x86_64)
|
||||
PLATFORM="linux/amd64"
|
||||
OUTPUT_NAME="didactyl_static_x86_64"
|
||||
;;
|
||||
aarch64|arm64)
|
||||
PLATFORM="linux/arm64"
|
||||
OUTPUT_NAME="didactyl_static_arm64"
|
||||
;;
|
||||
*)
|
||||
echo "WARNING: Unknown architecture: $ARCH"
|
||||
echo "Defaulting to linux/amd64"
|
||||
PLATFORM="linux/amd64"
|
||||
OUTPUT_NAME="didactyl_static_${ARCH}"
|
||||
;;
|
||||
esac
|
||||
# Detect architecture (or use explicit target platform override)
|
||||
if [ -n "$TARGET_PLATFORM" ]; then
|
||||
PLATFORM="$TARGET_PLATFORM"
|
||||
case "$PLATFORM" in
|
||||
linux/amd64)
|
||||
OUTPUT_NAME="didactyl_static_x86_64"
|
||||
;;
|
||||
linux/arm64)
|
||||
OUTPUT_NAME="didactyl_static_arm64"
|
||||
;;
|
||||
linux/arm/v7)
|
||||
OUTPUT_NAME="didactyl_static_armv7"
|
||||
;;
|
||||
linux/arm/v6)
|
||||
OUTPUT_NAME="didactyl_static_armv6"
|
||||
;;
|
||||
*)
|
||||
OUTPUT_NAME="didactyl_static_custom"
|
||||
;;
|
||||
esac
|
||||
else
|
||||
ARCH=$(uname -m)
|
||||
case "$ARCH" in
|
||||
x86_64)
|
||||
PLATFORM="linux/amd64"
|
||||
OUTPUT_NAME="didactyl_static_x86_64"
|
||||
;;
|
||||
aarch64|arm64)
|
||||
PLATFORM="linux/arm64"
|
||||
OUTPUT_NAME="didactyl_static_arm64"
|
||||
;;
|
||||
*)
|
||||
echo "WARNING: Unknown architecture: $ARCH"
|
||||
echo "Defaulting to linux/amd64"
|
||||
PLATFORM="linux/amd64"
|
||||
OUTPUT_NAME="didactyl_static_${ARCH}"
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Append _debug suffix to output name for debug builds
|
||||
if [ "$DEBUG_BUILD" = true ]; then
|
||||
@@ -89,6 +146,34 @@ echo "Building for platform: $PLATFORM"
|
||||
echo "Output binary: $OUTPUT_NAME"
|
||||
echo ""
|
||||
|
||||
# Verify Docker can execute the requested target platform (QEMU/binfmt for cross-arch)
|
||||
HOST_ARCH=$(uname -m)
|
||||
case "$HOST_ARCH" in
|
||||
x86_64) HOST_PLATFORM="linux/amd64" ;;
|
||||
aarch64|arm64) HOST_PLATFORM="linux/arm64" ;;
|
||||
armv7l) HOST_PLATFORM="linux/arm/v7" ;;
|
||||
armv6l) HOST_PLATFORM="linux/arm/v6" ;;
|
||||
*) HOST_PLATFORM="unknown" ;;
|
||||
esac
|
||||
|
||||
if [ "$HOST_PLATFORM" != "$PLATFORM" ]; then
|
||||
echo "Cross-architecture build detected: host=$HOST_PLATFORM target=$PLATFORM"
|
||||
echo "Checking Docker emulation support for $PLATFORM..."
|
||||
if ! $DOCKER_CMD run --rm --platform "$PLATFORM" alpine:3.19 uname -m > /dev/null 2>&1; then
|
||||
echo ""
|
||||
echo "ERROR: Docker cannot execute $PLATFORM containers on this host"
|
||||
echo "This usually means QEMU/binfmt is not configured."
|
||||
echo "Run this once, then retry:"
|
||||
echo " docker run --rm --privileged multiarch/qemu-user-static --reset -p yes"
|
||||
echo "Optional verify command:"
|
||||
echo " docker run --rm --platform $PLATFORM alpine:3.19 uname -m"
|
||||
echo ""
|
||||
exit 1
|
||||
fi
|
||||
echo "✓ Docker emulation for $PLATFORM is available"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Check if Alpine base image is cached
|
||||
echo "Checking for cached Alpine Docker image..."
|
||||
if ! docker images alpine:3.19 --format "{{.Repository}}:{{.Tag}}" | grep -q "alpine:3.19"; then
|
||||
@@ -118,6 +203,7 @@ echo " - Compile didactyl with full static linking"
|
||||
echo ""
|
||||
|
||||
$DOCKER_CMD build \
|
||||
--network host \
|
||||
--platform "$PLATFORM" \
|
||||
--build-arg DEBUG_BUILD=$DEBUG_BUILD \
|
||||
-f "$DOCKERFILE" \
|
||||
|
||||
@@ -40,10 +40,11 @@
|
||||
// triggered skills, HTTP API prompt runs, and local HTTP fetch tool.
|
||||
"tools": {
|
||||
"enabled": true,
|
||||
"max_turns": 8, // default max turns for normal agent tool loops
|
||||
"trigger_max_turns": 6, // max turns for triggered-skill executions
|
||||
"api_default_max_turns": 4, // default when HTTP body omits max_turns
|
||||
"api_max_turns_ceiling": 16, // hard cap applied to API-provided max_turns
|
||||
"max_turns": 20, // default max turns for normal agent tool loops
|
||||
"trigger_max_turns": 12, // max turns for triggered-skill executions
|
||||
"api_default_max_turns": 8, // default when HTTP body omits max_turns
|
||||
"api_max_turns_ceiling": 32, // hard cap applied to API-provided max_turns
|
||||
"stall_repeat_threshold": 3, // stop early when identical tool-call turns repeat this many times
|
||||
"local_http_fetch_default_timeout_seconds": 20,
|
||||
"local_http_fetch_max_timeout_seconds": 120,
|
||||
"shell": {
|
||||
|
||||
+36
-7
@@ -4,19 +4,48 @@ 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"
|
||||
REMOTE_HOST="ubuntu@laantungir.net"
|
||||
REMOTE_TARGETS=(
|
||||
"/home/simon/didactyl"
|
||||
"/home/didactyl/didactyl"
|
||||
)
|
||||
REMOTE_SERVICES=(
|
||||
"simon.service"
|
||||
"didactyl.service"
|
||||
)
|
||||
|
||||
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."
|
||||
if ! command -v ssh >/dev/null 2>&1; then
|
||||
echo "ERROR: ssh is not installed or not in PATH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Deploying $SOURCE_BINARY to $REMOTE_TARGET"
|
||||
rsync -avz --progress "$SOURCE_BINARY" "$REMOTE_TARGET"
|
||||
echo "Deployment complete."
|
||||
echo "Building static binary first..."
|
||||
"$SCRIPT_DIR/build_static.sh"
|
||||
|
||||
if [ ! -f "$SOURCE_BINARY" ]; then
|
||||
echo "ERROR: Build completed but source binary not found: $SOURCE_BINARY"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
REMOTE_STAGE="/tmp/didactyl_static_x86_64"
|
||||
|
||||
echo "Uploading $SOURCE_BINARY to staging path $REMOTE_HOST:$REMOTE_STAGE"
|
||||
rsync -avz --progress "$SOURCE_BINARY" "$REMOTE_HOST:$REMOTE_STAGE"
|
||||
|
||||
for target in "${REMOTE_TARGETS[@]}"; do
|
||||
echo "Installing staged binary to $target via sudo"
|
||||
ssh "$REMOTE_HOST" "sudo install -m 0755 '$REMOTE_STAGE' '$target'"
|
||||
done
|
||||
|
||||
echo "Restarting services on $REMOTE_HOST: ${REMOTE_SERVICES[*]}"
|
||||
ssh "$REMOTE_HOST" "sudo systemctl restart ${REMOTE_SERVICES[*]}"
|
||||
|
||||
echo "Cleaning up remote staging file $REMOTE_STAGE"
|
||||
ssh "$REMOTE_HOST" "rm -f '$REMOTE_STAGE'"
|
||||
|
||||
echo "Deployment complete for all targets and services."
|
||||
Executable
BIN
Binary file not shown.
+60
-24
@@ -53,7 +53,7 @@ show_usage() {
|
||||
echo " -M, --major: Increment major version, zero minor+patch (v0.1.0 → v1.0.0)"
|
||||
echo ""
|
||||
echo "RELEASE MODE (-r flag):"
|
||||
echo " - Build static binary using build_static.sh"
|
||||
echo " - Build static x86_64 and arm64 binaries using build_static.sh"
|
||||
echo " - Create source tarball"
|
||||
echo " - Git add, commit, push, and create Gitea release with assets"
|
||||
echo " - Can be combined with version increment flags"
|
||||
@@ -321,9 +321,9 @@ git_commit_and_push_no_tag() {
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to build release binary
|
||||
# Function to build release binaries
|
||||
build_release_binary() {
|
||||
print_status "Building release binary..."
|
||||
print_status "Building release binaries (x86_64 + arm64)..."
|
||||
|
||||
# Check if build_static.sh exists
|
||||
if [[ ! -f "build_static.sh" ]]; then
|
||||
@@ -331,12 +331,12 @@ build_release_binary() {
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Run the static build script
|
||||
if ./build_static.sh > /dev/null 2>&1; then
|
||||
print_success "Built static binary successfully"
|
||||
# Run the static build script for both platforms
|
||||
if ./build_static.sh --all-platforms > /dev/null 2>&1; then
|
||||
print_success "Built static binaries successfully"
|
||||
return 0
|
||||
else
|
||||
print_error "Failed to build static binary"
|
||||
print_error "Failed to build one or more static binaries"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
@@ -367,8 +367,9 @@ create_source_tarball() {
|
||||
# Function to upload release assets to Gitea
|
||||
upload_release_assets() {
|
||||
local release_id="$1"
|
||||
local binary_path="$2"
|
||||
local tarball_path="$3"
|
||||
local binary_x86_path="$2"
|
||||
local binary_arm64_path="$3"
|
||||
local tarball_path="$4"
|
||||
|
||||
print_status "Uploading release assets..."
|
||||
|
||||
@@ -383,9 +384,9 @@ upload_release_assets() {
|
||||
local assets_url="$api_url/releases/$release_id/assets"
|
||||
print_status "Assets URL: $assets_url"
|
||||
|
||||
# Upload binary
|
||||
if [[ -f "$binary_path" ]]; then
|
||||
print_status "Uploading binary: $(basename "$binary_path")"
|
||||
# Upload x86_64 binary
|
||||
if [[ -f "$binary_x86_path" ]]; then
|
||||
print_status "Uploading binary: $(basename "$binary_x86_path")"
|
||||
|
||||
# Retry loop for eventual consistency
|
||||
local max_attempts=3
|
||||
@@ -394,11 +395,11 @@ upload_release_assets() {
|
||||
print_status "Upload attempt $attempt/$max_attempts"
|
||||
local binary_response=$(curl -fS -X POST "$assets_url" \
|
||||
-H "Authorization: token $token" \
|
||||
-F "attachment=@$binary_path;filename=$(basename "$binary_path")" \
|
||||
-F "name=$(basename "$binary_path")")
|
||||
-F "attachment=@$binary_x86_path;filename=$(basename "$binary_x86_path")" \
|
||||
-F "name=$(basename "$binary_x86_path")")
|
||||
|
||||
if echo "$binary_response" | grep -q '"id"'; then
|
||||
print_success "Uploaded binary successfully"
|
||||
print_success "Uploaded x86_64 binary successfully"
|
||||
break
|
||||
else
|
||||
print_warning "Upload attempt $attempt failed"
|
||||
@@ -406,7 +407,37 @@ upload_release_assets() {
|
||||
print_status "Retrying in 2 seconds..."
|
||||
sleep 2
|
||||
else
|
||||
print_error "Failed to upload binary after $max_attempts attempts"
|
||||
print_error "Failed to upload x86_64 binary after $max_attempts attempts"
|
||||
print_error "Response: $binary_response"
|
||||
fi
|
||||
fi
|
||||
((attempt++))
|
||||
done
|
||||
fi
|
||||
|
||||
# Upload arm64 binary
|
||||
if [[ -f "$binary_arm64_path" ]]; then
|
||||
print_status "Uploading binary: $(basename "$binary_arm64_path")"
|
||||
|
||||
local max_attempts=3
|
||||
local attempt=1
|
||||
while [[ $attempt -le $max_attempts ]]; do
|
||||
print_status "Upload attempt $attempt/$max_attempts"
|
||||
local binary_response=$(curl -fS -X POST "$assets_url" \
|
||||
-H "Authorization: token $token" \
|
||||
-F "attachment=@$binary_arm64_path;filename=$(basename "$binary_arm64_path")" \
|
||||
-F "name=$(basename "$binary_arm64_path")")
|
||||
|
||||
if echo "$binary_response" | grep -q '"id"'; then
|
||||
print_success "Uploaded arm64 binary successfully"
|
||||
break
|
||||
else
|
||||
print_warning "Upload attempt $attempt failed"
|
||||
if [[ $attempt -lt $max_attempts ]]; then
|
||||
print_status "Retrying in 2 seconds..."
|
||||
sleep 2
|
||||
else
|
||||
print_error "Failed to upload arm64 binary after $max_attempts attempts"
|
||||
print_error "Response: $binary_response"
|
||||
fi
|
||||
fi
|
||||
@@ -515,16 +546,21 @@ main() {
|
||||
# Commit and push
|
||||
git_commit_and_push_no_tag
|
||||
|
||||
# Build release binary
|
||||
# Build release binaries
|
||||
local binary_x86_path=""
|
||||
local binary_arm64_path=""
|
||||
if build_release_binary; then
|
||||
local binary_path="build/didactyl_static_x86_64"
|
||||
[[ -f "build/didactyl_static_x86_64" ]] && binary_x86_path="build/didactyl_static_x86_64"
|
||||
[[ -f "build/didactyl_static_arm64" ]] && binary_arm64_path="build/didactyl_static_arm64"
|
||||
else
|
||||
print_warning "Binary build failed, continuing with release creation"
|
||||
if [[ -f "build/didactyl_static_x86_64" ]]; then
|
||||
print_status "Using existing binary from previous build"
|
||||
binary_path="build/didactyl_static_x86_64"
|
||||
else
|
||||
binary_path=""
|
||||
print_status "Using existing x86_64 binary from previous build"
|
||||
binary_x86_path="build/didactyl_static_x86_64"
|
||||
fi
|
||||
if [[ -f "build/didactyl_static_arm64" ]]; then
|
||||
print_status "Using existing arm64 binary from previous build"
|
||||
binary_arm64_path="build/didactyl_static_arm64"
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -540,8 +576,8 @@ main() {
|
||||
local release_id=""
|
||||
if release_id=$(create_gitea_release); then
|
||||
if [[ "$release_id" =~ ^[0-9]+$ ]]; then
|
||||
if [[ -n "$release_id" && (-n "$binary_path" || -n "$tarball_path") ]]; then
|
||||
upload_release_assets "$release_id" "$binary_path" "$tarball_path"
|
||||
if [[ -n "$release_id" && (-n "$binary_x86_path" || -n "$binary_arm64_path" || -n "$tarball_path") ]]; then
|
||||
upload_release_assets "$release_id" "$binary_x86_path" "$binary_arm64_path" "$tarball_path"
|
||||
fi
|
||||
print_success "Release $NEW_VERSION completed successfully!"
|
||||
else
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
# Enhanced Existing Agent Wizard Flow
|
||||
|
||||
## Problem
|
||||
|
||||
When choosing "existing agent" in the wizard, the current flow:
|
||||
1. Asks for nsec
|
||||
2. Recovers kind 10002 relay list from Nostr
|
||||
3. Immediately boots the agent
|
||||
|
||||
This is insufficient when installing an existing agent on a **new server** because:
|
||||
- You cannot review or change the admin pubkey
|
||||
- You cannot review or change the LLM provider/model/API key
|
||||
- You cannot install a systemd service with a dedicated user
|
||||
- You cannot see what config was recovered from Nostr
|
||||
- The agent name is not recovered from the kind 0 profile
|
||||
|
||||
## Current Code
|
||||
|
||||
- [`existing_agent_flow()`](src/setup_wizard.c:1795) — 22 lines, minimal recovery
|
||||
- [`recover_existing_config_from_nostr()`](src/setup_wizard.c:792) — only recovers kind 10002 relays
|
||||
- Returns `SETUP_WIZARD_RC_EXISTING` (2) which does NOT set `bootstrap_mode`
|
||||
- In `main.c`, existing agents skip `reconcile_startup_events()` unless `first_run` is detected
|
||||
|
||||
## Data Available on Nostr for an Existing Agent
|
||||
|
||||
| Data | Kind | Storage | Recovery Method |
|
||||
|------|------|---------|-----------------|
|
||||
| Relay list | 10002 | Public tags | `query_and_extract_kind10002_relays()` |
|
||||
| Agent profile/name | 0 | Public JSON content | Query kind 0 by agent pubkey |
|
||||
| LLM config | 30078 d=llm_config | NIP-44 encrypted to self | `fetch_self_config_plaintext()` |
|
||||
| Agent config - admin pubkey, DM protocol | 30078 d=agent_config | NIP-44 encrypted to self | `fetch_self_config_plaintext()` |
|
||||
| Default skill | 31124 d=didactyl-default | Public content | Query kind 31124 by agent pubkey |
|
||||
| Adoption list | 10123 | Public tags | Query kind 10123 by agent pubkey |
|
||||
|
||||
## Proposed Enhanced Flow
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[Enter nsec] --> B[Connect to default relays]
|
||||
B --> C[Recover kind 10002 relay list]
|
||||
C --> D{Relay list found?}
|
||||
D -->|No| E{Offer to create new agent with this nsec}
|
||||
E -->|Yes| E2[Jump to new_agent_flow with nsec pre-loaded]
|
||||
E -->|No| E3[Return to main menu]
|
||||
D -->|Yes| F[Reconnect with recovered relays]
|
||||
F --> G[Recover all config from Nostr]
|
||||
G --> H[Display recovered config summary]
|
||||
H --> I{Review each setting}
|
||||
I -->|Keep all| J[Review summary + launch options]
|
||||
I -->|Change admin| K[Prompt new admin pubkey]
|
||||
I -->|Change LLM| L[Prompt LLM config]
|
||||
I -->|Change relays| M[Prompt relay config]
|
||||
K --> I
|
||||
L --> I
|
||||
M --> I
|
||||
J --> N{Launch option}
|
||||
N -->|Boot now| O[Return EXISTING]
|
||||
N -->|Install systemd| P[Install dedicated-user service]
|
||||
N -->|Quit| Q[Exit]
|
||||
```
|
||||
|
||||
### Step-by-step
|
||||
|
||||
#### Step 1: Identity — Enter nsec
|
||||
Same as current. Derive keys from nsec.
|
||||
|
||||
#### Step 2: Recovery — Connect and fetch config from Nostr
|
||||
1. Init nostr handler with default relays
|
||||
2. Wait for relay connections
|
||||
3. Query kind 10002 for relay list — if not found, offer to create a new agent with this nsec (jump to `new_agent_flow` with keys pre-loaded, skipping identity step)
|
||||
4. Cleanup and re-init with recovered relays
|
||||
5. Wait for relay connections on recovered relays
|
||||
6. Query kind 0 for agent profile — extract display_name/name
|
||||
7. Query kind 30078 d=llm_config — decrypt and parse LLM settings
|
||||
8. Query kind 30078 d=agent_config — decrypt and parse admin pubkey + DM protocol
|
||||
9. Cleanup nostr handler
|
||||
|
||||
#### Step 3: Review — Present recovered config
|
||||
Display all recovered values:
|
||||
```
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ Existing Agent -- Recovered Configuration │
|
||||
├─────────────────────────────────────────────┤
|
||||
│ Agent name: Simon │
|
||||
│ Identity: b27072b7fc2edf45... │
|
||||
│ Admin: a1b2c3d4e5f6... │
|
||||
│ LLM Provider: ppq │
|
||||
│ LLM Model: claude-haiku-4.5 │
|
||||
│ LLM Base URL: https://api.ppq.ai │
|
||||
│ LLM API Key: sk-...**** │
|
||||
│ DM Protocol: nip04 │
|
||||
│ Relays: 5 configured │
|
||||
└─────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
Then offer a menu:
|
||||
```
|
||||
[a] change Admin pubkey
|
||||
[l] change LLM provider/model/key
|
||||
[r] change Relay configuration
|
||||
[c] continue with these settings
|
||||
[q] quit
|
||||
```
|
||||
|
||||
Each change option reuses the existing prompt functions (`prompt_admin_pubkey`, `prompt_llm_config`, `prompt_relay_configuration`) but with context-appropriate headers.
|
||||
|
||||
After any change, redisplay the summary and menu.
|
||||
|
||||
#### Step 4: Launch — Boot or install systemd
|
||||
Same as the new-agent flow's final step:
|
||||
```
|
||||
[b] boot the agent now
|
||||
[i] install dedicated-user systemd service and boot
|
||||
[q] quit
|
||||
```
|
||||
|
||||
The systemd install reuses `install_system_service_with_dedicated_user()`.
|
||||
|
||||
### Return Codes
|
||||
|
||||
- If user chooses "boot now": return `SETUP_WIZARD_RC_EXISTING` (2) — same as current
|
||||
- If user chooses "install systemd": return `SETUP_WIZARD_RC_EXIT` (1) — agent runs as systemd service
|
||||
- If user changed config values: the `main()` flow should still work because `recover_missing_runtime_config_from_nostr()` at line 1108 will fill in any gaps, and the existing agent path at line 1163 loads system context from adopted skills
|
||||
|
||||
### Key Consideration: bootstrap_mode for changed configs
|
||||
|
||||
If the user changes LLM or admin config in the wizard, those changes need to be persisted back to Nostr. Currently, `persist_runtime_config_to_nostr()` is only called when `bootstrap_mode || first_run`.
|
||||
|
||||
**Solution**: When the existing-agent wizard detects that config was changed, return `SETUP_WIZARD_RC_BOOTSTRAP` (0) instead of `SETUP_WIZARD_RC_EXISTING` (2). This triggers the full reconcile path which persists the updated config.
|
||||
|
||||
## Files to Modify
|
||||
|
||||
1. **`src/setup_wizard.c`**:
|
||||
- Add `recover_full_config_from_nostr()` — fetches kind 0, kind 30078 llm_config, kind 30078 agent_config
|
||||
- Add `query_self_kind0_name()` — queries agent's own kind 0 profile for name
|
||||
- Add `fetch_and_decrypt_self_config()` — replicates `fetch_self_config_plaintext()` logic from main.c for use in wizard context
|
||||
- Rewrite `existing_agent_flow()` with the enhanced multi-step flow
|
||||
- Make `prompt_admin_pubkey()`, `prompt_llm_config()`, `prompt_relay_configuration()` accept a context string parameter for the page header, or add wrapper versions for the existing-agent context
|
||||
|
||||
2. **`src/main.c`**:
|
||||
- Potentially expose `fetch_self_config_plaintext()`, `apply_recalled_llm_config()`, `apply_recalled_agent_config()` as non-static, OR duplicate the logic in setup_wizard.c
|
||||
- Better approach: move these to a shared location or make them accessible via a header
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The wizard already calls `nostr_handler_init()` / `nostr_handler_cleanup()` for validation queries. The enhanced flow will do the same but with two init/cleanup cycles: first with default relays to get kind 10002, then with recovered relays to get everything else.
|
||||
- The `prompt_admin_pubkey()` and `prompt_llm_config()` functions currently have hardcoded step headers like "Step 3 of 7". These should be parameterized or have existing-agent variants.
|
||||
- The `fetch_self_config_plaintext()` function in main.c requires an active nostr handler. The wizard will need to have the handler initialized when calling it.
|
||||
- API key display should be masked — show only first 4 and last 4 characters.
|
||||
@@ -0,0 +1,69 @@
|
||||
# Fix: Kind 30078 `d=agent_config` Publish Gaps
|
||||
|
||||
## Problem
|
||||
|
||||
The agent stores its admin pubkey and DM protocol in a NIP-44 self-encrypted kind 30078 replaceable event (`d=agent_config`). This event is critical for the `--nsec`-only startup path (used by systemd services) to recover the admin identity.
|
||||
|
||||
**Multiple startup paths fail to publish this event**, causing:
|
||||
- Agents installed via wizard "Install Service" to lose their admin pubkey on restart
|
||||
- Agents recovering on new servers to use stale/old admin pubkeys from relays
|
||||
- Agents with genesis files to silently ignore newer kind 30078 values
|
||||
|
||||
## Affected Files
|
||||
|
||||
- `src/setup_wizard.c` — wizard flows
|
||||
- `src/main.c` — main startup logic
|
||||
|
||||
## Fixes
|
||||
|
||||
### Fix A: `new_agent_flow` — publish before service install
|
||||
**File:** `src/setup_wizard.c` ~line 2184
|
||||
**Problem:** When user chooses "Install systemd service" in new agent flow, `persist_runtime_config_to_nostr_wizard_online` is never called. The service starts with `--nsec` only and has no kind 30078 to recover from.
|
||||
**Fix:** Call `persist_runtime_config_to_nostr_wizard_online(cfg)` before `install_system_service_with_dedicated_user()`. Fail the install if publish fails (same pattern as existing_agent_flow).
|
||||
|
||||
### Fix B: `new_agent_flow` — publish before "boot now" return
|
||||
**File:** `src/setup_wizard.c` ~line 2177
|
||||
**Problem:** When user chooses "Boot now", the wizard returns `SETUP_WIZARD_RC_BOOTSTRAP` and relies on `main()` to publish later. This works but is fragile — if the bootstrap publish in `main()` fails, the kind 30078 is never created.
|
||||
**Fix:** Call `persist_runtime_config_to_nostr_wizard_online(cfg)` before returning 0. This is belt-and-suspenders — `main()` will also publish, but the wizard ensures it happens at least once. If the wizard publish fails, log a warning but continue (non-fatal since main will retry).
|
||||
|
||||
### Fix C: `existing_agent_flow` — always publish before service install
|
||||
**File:** `src/setup_wizard.c` ~line 2368
|
||||
**Problem:** `persist_runtime_config_to_nostr_wizard_online` is only called `if (config_changed)`. If the user accepts the recovered config as-is, the kind 30078 is not re-published. The event may have been lost from relays.
|
||||
**Fix:** Remove the `config_changed` gate. Always call `persist_runtime_config_to_nostr_wizard_online(cfg)` before installing the service. Kind 30078 is a replaceable event, so re-publishing is safe and idempotent.
|
||||
|
||||
### Fix D: `existing_agent_flow` — always return BOOTSTRAP
|
||||
**File:** `src/setup_wizard.c` ~line 2364
|
||||
**Problem:** When user chooses "Boot now" and `config_changed == 0`, the flow returns `SETUP_WIZARD_RC_EXISTING` which means `bootstrap_mode = 0` in `main()`. If `first_run = 0` (kind 10002 exists), `persist_runtime_config_to_nostr` is skipped.
|
||||
**Fix:** Always return `SETUP_WIZARD_RC_BOOTSTRAP` from the "boot now" path, regardless of `config_changed`. This ensures `main()` always re-publishes the kind 30078. The wizard has all the config in memory — it should always be treated as authoritative.
|
||||
|
||||
### Fix E: `persist_runtime_config_to_nostr_wizard_online` — ensure relay delivery
|
||||
**File:** `src/setup_wizard.c` ~line 1159
|
||||
**Problem:** The function publishes the event and immediately calls `nostr_handler_cleanup()`. The event may not have been delivered to relays yet (fire-and-forget).
|
||||
**Fix:** Add a brief poll loop (e.g., 2-3 seconds of `nostr_handler_poll()`) after the publish call and before `nostr_handler_cleanup()` to give the event time to propagate. This matches the pattern used elsewhere for relay operations.
|
||||
|
||||
### Fix F: `main()` — always fetch kind 30078 and compare
|
||||
**File:** `src/main.c` ~line 847 (`recover_missing_runtime_config_from_nostr`)
|
||||
**Problem:** The recovery only runs when `admin_config_is_complete()` returns false. If a genesis file provides an admin pubkey, the kind 30078 is never checked, even if it has a different (newer) value.
|
||||
**Fix:** Always fetch the kind 30078 `d=agent_config` regardless of whether the genesis already provided values. Compare the fetched admin_pubkey with the loaded one. If they differ, log a `DEBUG_WARN` with both values. Genesis wins (operator intent), but the warning makes the conflict visible in logs.
|
||||
|
||||
### Fix G: `main()` — always re-publish kind 30078
|
||||
**File:** `src/main.c` ~line 1152
|
||||
**Problem:** `persist_runtime_config_to_nostr` only runs when `bootstrap_mode || first_run`. On subsequent runs, the kind 30078 is never refreshed. If relays purge the event, it's gone.
|
||||
**Fix:** Move `persist_runtime_config_to_nostr(&cfg)` outside the `if (bootstrap_mode || first_run)` block so it runs on every startup. Kind 30078 is a replaceable event — re-publishing is safe, idempotent, and ensures relay persistence. Log the result but don't fail startup if it doesn't succeed.
|
||||
|
||||
## Execution Order
|
||||
|
||||
1. Fix E first (relay delivery) — this makes all other wizard publishes more reliable
|
||||
2. Fixes A + B (new_agent_flow) — the most critical bug
|
||||
3. Fixes C + D (existing_agent_flow) — second most critical
|
||||
4. Fix G (always re-publish in main) — ensures long-term relay persistence
|
||||
5. Fix F (conflict detection) — nice-to-have logging improvement
|
||||
|
||||
## Testing
|
||||
|
||||
- Wizard → New Agent → Install Service: verify kind 30078 exists on relays after service starts
|
||||
- Wizard → New Agent → Boot: verify kind 30078 exists on relays
|
||||
- Wizard → Existing Agent → Install Service (no changes): verify kind 30078 re-published
|
||||
- Wizard → Existing Agent → Boot (no changes): verify kind 30078 re-published
|
||||
- `--nsec` only restart: verify kind 30078 recovered and re-published
|
||||
- `--config genesis.jsonc` with different admin than kind 30078: verify warning logged, genesis wins, kind 30078 updated
|
||||
@@ -0,0 +1,146 @@
|
||||
# Fix: Local File Persistence & Stall Diagnostic Improvements
|
||||
|
||||
## Context
|
||||
|
||||
The agent got stuck in a stall loop when trying to create a skill via DM. The root cause is `max_tokens=512` truncating the LLM's tool call JSON. The admin tried to fix it with `/model_set {"max_tokens": 10000}` but got `"failed to persist llm config to config file"` because `model_set` tries to write to a local config file that doesn't exist in the Nostr-native deployment model.
|
||||
|
||||
Investigation revealed two tools that incorrectly persist state to local files instead of kind 30078 Nostr events, plus missing diagnostic information in the stall detector.
|
||||
|
||||
## Hardcoded File Locations to Remove
|
||||
|
||||
### tool_model.c
|
||||
- `ctx->cfg->config_path` — references a local config file (genesis.jsonc or config.jsonc)
|
||||
- The entire `persist_llm_config()` function (lines 82-139) reads/writes this file
|
||||
- The `read_entire_file_local()` helper (lines 42-80) exists only for this purpose
|
||||
|
||||
### tool_task.c
|
||||
- `"tasks.json"` — hardcoded at line 188: `build_tool_path_local(ctx, "tasks.json", ...)`
|
||||
- `tasks_load_root_local()` (lines 82-141) reads from this file
|
||||
- `tasks_save_root_local()` (lines 143-159) writes to this file
|
||||
|
||||
### tools_schema.c
|
||||
- Line 985: `"Update active LLM configuration and persist it to config.jsonc"` — mentions config.jsonc
|
||||
- Line 1285: `"Build current task list context block from tasks.json"` — mentions tasks.json
|
||||
- Line 1314: `"Manage agent short-term task memory stored in tasks.json"` — mentions tasks.json
|
||||
|
||||
## Fixes
|
||||
|
||||
### Fix 1: model_set — Persist to Kind 30078
|
||||
|
||||
**File:** `src/tools/tool_model.c`
|
||||
|
||||
Replace `persist_llm_config()` (lines 82-139) with a function that:
|
||||
1. Builds a JSON object with LLM config fields (provider, model, base_url, max_tokens, temperature — NOT api_key for security)
|
||||
2. NIP-44 self-encrypts it using the same pattern as `config_store` in `tool_config.c`
|
||||
3. Publishes as kind 30078 with tags `["d", "llm_config"]` and `["app", "didactyl"]`
|
||||
|
||||
Delete the `read_entire_file_local()` helper (lines 42-80) and the `json_object_set_string()` helper (lines 33-40) — they only exist for the file-based persist.
|
||||
|
||||
Also update `execute_model_set()` (line 234) to call the new Nostr-based persist, and return success with a warning if persist fails (since runtime update already succeeded at line 229).
|
||||
|
||||
### Fix 2: model_set — Don't Fail When Only Persist Fails
|
||||
|
||||
**File:** `src/tools/tool_model.c`, lines 229-236
|
||||
|
||||
Currently:
|
||||
```c
|
||||
if (llm_set_config(&cfg) != 0) {
|
||||
return json_error_local("failed to update runtime llm config");
|
||||
}
|
||||
ctx->cfg->llm = cfg;
|
||||
if (persist_llm_config(ctx, &cfg) != 0) {
|
||||
return json_error_local("failed to persist llm config to config file");
|
||||
}
|
||||
```
|
||||
|
||||
Change to: return success with a `"persist_warning"` field if persist fails, since the runtime update already took effect.
|
||||
|
||||
### Fix 3: task_manage — Persist to Kind 30078
|
||||
|
||||
**File:** `src/tools/tool_task.c`
|
||||
|
||||
Replace `tasks_load_root_local()` and `tasks_save_root_local()` with:
|
||||
- `tasks_load_root_nostr()` — queries kind 30078 `d=tasks` from self, NIP-44 decrypts, parses JSON
|
||||
- `tasks_save_root_nostr()` — serializes JSON, NIP-44 self-encrypts, publishes kind 30078 `d=tasks`
|
||||
|
||||
This follows the exact same pattern as `config_store`/`config_recall` in `tool_config.c` and `memory_save`/`memory_recall` in `tool_memory.c`.
|
||||
|
||||
Remove the hardcoded `"tasks.json"` string at line 188 and the `build_tool_path_local()` call for it.
|
||||
|
||||
Remove `tasks_path` from the response JSON at line 424.
|
||||
|
||||
### Fix 4: Update Schema Descriptions
|
||||
|
||||
**File:** `src/tools/tools_schema.c`
|
||||
|
||||
- Line 985: Change `"Update active LLM configuration and persist it to config.jsonc"` to `"Update active LLM configuration and persist it to Nostr"`
|
||||
- Line 1285: Change `"Build current task list context block from tasks.json"` to `"Build current task list context block from agent task memory"`
|
||||
- Line 1314: Change `"Manage agent short-term task memory stored in tasks.json (list/add/update/remove/clear/replace)"` to `"Manage agent short-term task memory stored on Nostr (list/add/update/remove/clear/replace)"`
|
||||
|
||||
### Fix 5: Add finish_reason to LLM Response
|
||||
|
||||
**File:** `src/llm.h`
|
||||
|
||||
Add `char* finish_reason;` to `llm_response_t`:
|
||||
```c
|
||||
typedef struct {
|
||||
char* content;
|
||||
llm_tool_call_t* tool_calls;
|
||||
int tool_call_count;
|
||||
char* finish_reason;
|
||||
} llm_response_t;
|
||||
```
|
||||
|
||||
**File:** `src/llm.c`
|
||||
|
||||
In `parse_llm_response()` (line 276), after extracting `msg` from `choices[0]`, extract `finish_reason`:
|
||||
```c
|
||||
cJSON* fr = first ? cJSON_GetObjectItemCaseSensitive(first, "finish_reason") : NULL;
|
||||
if (fr && cJSON_IsString(fr) && fr->valuestring) {
|
||||
out->finish_reason = strdup(fr->valuestring);
|
||||
}
|
||||
```
|
||||
|
||||
In `llm_response_free()`, add `free(response->finish_reason);`.
|
||||
|
||||
### Fix 6: Add max_tokens Truncation Hint to Stall Diagnostic
|
||||
|
||||
**File:** `src/agent.c`
|
||||
|
||||
In the stall detection block at line 2217, before freeing `resp`, check `finish_reason`:
|
||||
```c
|
||||
if (repeated_tool_turns >= stall_repeat_threshold) {
|
||||
int truncated = resp.finish_reason && strcmp(resp.finish_reason, "length") == 0;
|
||||
exited_on_stall = 1;
|
||||
llm_response_free(&resp);
|
||||
break;
|
||||
}
|
||||
```
|
||||
|
||||
In `notify_admin_limit_diagnostic()` (line 106), add fields:
|
||||
- `possible_cause` — "max_tokens_truncation" when finish_reason was "length"
|
||||
- `current_max_tokens` — the current max_tokens value
|
||||
- `hint` — "Increase max_tokens: /model_set {\"max_tokens\": 4096}"
|
||||
|
||||
**File:** `src/http_api.c`
|
||||
|
||||
Same changes in the HTTP API tool loop stall detection at line 966.
|
||||
|
||||
## Execution Order
|
||||
|
||||
1. Fix 5 (finish_reason in llm_response_t) — no dependencies
|
||||
2. Fix 6 (stall diagnostic) — depends on Fix 5
|
||||
3. Fix 1 (model_set persist to Nostr) — independent
|
||||
4. Fix 2 (model_set graceful failure) — can be done with Fix 1
|
||||
5. Fix 3 (task_manage persist to Nostr) — independent
|
||||
6. Fix 4 (schema descriptions) — do last, trivial
|
||||
|
||||
## Files Modified
|
||||
|
||||
- `src/llm.h` — add finish_reason field
|
||||
- `src/llm.c` — parse finish_reason, free it
|
||||
- `src/agent.c` — stall diagnostic with truncation hint
|
||||
- `src/http_api.c` — stall diagnostic with truncation hint
|
||||
- `src/tools/tool_model.c` — replace file persist with kind 30078, graceful failure
|
||||
- `src/tools/tool_task.c` — replace file I/O with kind 30078
|
||||
- `src/tools/tools_schema.c` — update 3 descriptions
|
||||
+164
-7
@@ -79,6 +79,72 @@ static uint64_t message_fingerprint(const char* sender_pubkey_hex, const char* m
|
||||
return a ^ (b + 0x9e3779b97f4a7c15ULL + (a << 6) + (a >> 2));
|
||||
}
|
||||
|
||||
static uint64_t tool_calls_fingerprint(const llm_response_t* resp) {
|
||||
if (!resp || resp->tool_call_count <= 0 || !resp->tool_calls) {
|
||||
return 1469598103934665603ULL;
|
||||
}
|
||||
|
||||
uint64_t h = 1469598103934665603ULL;
|
||||
for (int i = 0; i < resp->tool_call_count; i++) {
|
||||
const llm_tool_call_t* tc = &resp->tool_calls[i];
|
||||
uint64_t name_h = fnv1a64(tc->name ? tc->name : "");
|
||||
uint64_t args_h = fnv1a64(tc->arguments_json ? tc->arguments_json : "{}");
|
||||
uint64_t part = name_h ^ (args_h + 0x9e3779b97f4a7c15ULL + (name_h << 6) + (name_h >> 2));
|
||||
h ^= part + (uint64_t)(i + 1) * 1099511628211ULL;
|
||||
h *= 1099511628211ULL;
|
||||
}
|
||||
|
||||
h ^= (uint64_t)resp->tool_call_count;
|
||||
h *= 1099511628211ULL;
|
||||
return h;
|
||||
}
|
||||
|
||||
static void notify_admin_limit_diagnostic(const char* source,
|
||||
const char* reason,
|
||||
const char* subject,
|
||||
int max_turns,
|
||||
int turns_run,
|
||||
int stall_repeat_threshold,
|
||||
int repeated_tool_turns,
|
||||
int current_max_tokens,
|
||||
const char* possible_cause,
|
||||
const char* hint,
|
||||
const char* final_answer) {
|
||||
if (!g_cfg || g_cfg->admin.pubkey[0] == '\0') {
|
||||
return;
|
||||
}
|
||||
|
||||
char diag[1800];
|
||||
snprintf(diag,
|
||||
sizeof(diag),
|
||||
"⚠️ Didactyl limit diagnostic\n"
|
||||
"source=%s\n"
|
||||
"reason=%s\n"
|
||||
"subject=%s\n"
|
||||
"max_turns=%d\n"
|
||||
"turns_run=%d\n"
|
||||
"stall_repeat_threshold=%d\n"
|
||||
"repeated_tool_turns=%d\n"
|
||||
"current_max_tokens=%d\n"
|
||||
"possible_cause=%s\n"
|
||||
"hint=%s\n"
|
||||
"final_answer_preview=%.*s",
|
||||
source ? source : "unknown",
|
||||
reason ? reason : "unknown",
|
||||
subject ? subject : "n/a",
|
||||
max_turns,
|
||||
turns_run,
|
||||
stall_repeat_threshold,
|
||||
repeated_tool_turns,
|
||||
current_max_tokens,
|
||||
possible_cause ? possible_cause : "n/a",
|
||||
hint ? hint : "n/a",
|
||||
360,
|
||||
final_answer ? final_answer : "");
|
||||
|
||||
(void)nostr_handler_send_dm_auto(g_cfg->admin.pubkey, diag);
|
||||
}
|
||||
|
||||
static int agent_message_is_debounced(const char* sender_pubkey_hex, const char* message) {
|
||||
time_t now = time(NULL);
|
||||
uint64_t fp = message_fingerprint(sender_pubkey_hex, message);
|
||||
@@ -371,6 +437,15 @@ static char* format_result_markdown_local(const char* raw_result) {
|
||||
return strdup(raw_result);
|
||||
}
|
||||
|
||||
if (cJSON_IsObject(root)) {
|
||||
cJSON* content = cJSON_GetObjectItemCaseSensitive(root, "content");
|
||||
if (content && cJSON_IsString(content) && content->valuestring) {
|
||||
char* direct = strdup(content->valuestring);
|
||||
cJSON_Delete(root);
|
||||
return direct ? direct : strdup(raw_result);
|
||||
}
|
||||
}
|
||||
|
||||
size_t cap = strlen(raw_result) * 2U + 512U;
|
||||
if (cap < 2048U) cap = 2048U;
|
||||
size_t used = 0U;
|
||||
@@ -508,10 +583,15 @@ static char* build_slash_help_all_json(void) {
|
||||
|
||||
char* adopted_json = tools_execute(&g_tools_ctx, "adopted_skills", "{}");
|
||||
cJSON* adopted_root = adopted_json ? cJSON_Parse(adopted_json) : NULL;
|
||||
cJSON* adoption_events_content = adopted_root ? cJSON_GetObjectItemCaseSensitive(adopted_root, "content") : 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;
|
||||
const char* adoption_events_text = NULL;
|
||||
if (adoption_events_content && cJSON_IsString(adoption_events_content) && adoption_events_content->valuestring) {
|
||||
adoption_events_text = adoption_events_content->valuestring;
|
||||
} else if (adoption_events_json && cJSON_IsString(adoption_events_json) && adoption_events_json->valuestring) {
|
||||
adoption_events_text = adoption_events_json->valuestring;
|
||||
}
|
||||
cJSON* adoption_events = adoption_events_text ? cJSON_Parse(adoption_events_text) : NULL;
|
||||
|
||||
int adopted_count = 0;
|
||||
if (adoption_events && cJSON_IsArray(adoption_events) && cJSON_GetArraySize(adoption_events) > 0) {
|
||||
@@ -661,8 +741,11 @@ static int handle_slash_command(const char* sender_pubkey_hex, const char* messa
|
||||
result_json = strdup("{\"success\":false,\"error\":\"direct tool execution failed\"}");
|
||||
}
|
||||
|
||||
char* markdown_result = format_result_markdown_local(result_json);
|
||||
const char* dm_payload = markdown_result ? markdown_result : result_json;
|
||||
size_t tool_name_len = strlen(tool_name);
|
||||
int send_raw_json = (tool_name_len >= 4U && strcmp(tool_name + tool_name_len - 4U, "_get") == 0);
|
||||
|
||||
char* markdown_result = send_raw_json ? NULL : format_result_markdown_local(result_json);
|
||||
const char* dm_payload = send_raw_json ? result_json : (markdown_result ? markdown_result : result_json);
|
||||
|
||||
size_t log_cap = strlen(message) + strlen(result_json ? result_json : "") + strlen(dm_payload ? dm_payload : "") + 192U;
|
||||
char* log_payload = (char*)malloc(log_cap);
|
||||
@@ -1815,7 +1898,10 @@ void agent_on_trigger(const char* skill_d_tag,
|
||||
? g_cfg->tools.trigger_max_turns
|
||||
: (g_cfg->tools.max_turns > 0 ? g_cfg->tools.max_turns : 8);
|
||||
|
||||
int turns_run = 0;
|
||||
int trigger_completed = 0;
|
||||
for (int turn = 0; turn < max_turns; turn++) {
|
||||
turns_run = turn + 1;
|
||||
char* messages_json = cJSON_PrintUnformatted(messages);
|
||||
if (!messages_json) {
|
||||
break;
|
||||
@@ -1836,6 +1922,7 @@ void agent_on_trigger(const char* skill_d_tag,
|
||||
}
|
||||
|
||||
if (resp.tool_call_count <= 0) {
|
||||
trigger_completed = 1;
|
||||
llm_response_free(&resp);
|
||||
break;
|
||||
}
|
||||
@@ -1871,6 +1958,20 @@ void agent_on_trigger(const char* skill_d_tag,
|
||||
llm_response_free(&resp);
|
||||
}
|
||||
|
||||
if (!trigger_completed && turns_run >= max_turns) {
|
||||
notify_admin_limit_diagnostic("trigger_skill_loop",
|
||||
"max_turns_exhausted",
|
||||
skill_d_tag,
|
||||
max_turns,
|
||||
turns_run,
|
||||
g_cfg->tools.stall_repeat_threshold > 1 ? g_cfg->tools.stall_repeat_threshold : 3,
|
||||
0,
|
||||
g_cfg->llm.max_tokens,
|
||||
NULL,
|
||||
NULL,
|
||||
"");
|
||||
}
|
||||
|
||||
if (g_trigger_manager) {
|
||||
(void)trigger_manager_fire_chains(g_trigger_manager,
|
||||
skill_d_tag,
|
||||
@@ -2097,10 +2198,17 @@ void agent_on_message(const char* sender_pubkey_hex,
|
||||
cJSON_AddNumberToObject(live_user_msg, "_ts", (double)time(NULL));
|
||||
}
|
||||
|
||||
int max_turns = g_cfg->tools.max_turns > 0 ? g_cfg->tools.max_turns : 8;
|
||||
int max_turns = g_cfg->tools.max_turns > 0 ? g_cfg->tools.max_turns : 20;
|
||||
int stall_repeat_threshold = g_cfg->tools.stall_repeat_threshold > 1 ? g_cfg->tools.stall_repeat_threshold : 3;
|
||||
uint64_t last_tool_fp = 0;
|
||||
int repeated_tool_turns = 0;
|
||||
int exited_on_stall = 0;
|
||||
int stall_due_to_length = 0;
|
||||
char* final_answer_owned = NULL;
|
||||
int turns_run = 0;
|
||||
|
||||
for (int turn = 0; turn < max_turns; turn++) {
|
||||
turns_run = turn + 1;
|
||||
char* messages_json = cJSON_PrintUnformatted(messages);
|
||||
if (!messages_json) {
|
||||
break;
|
||||
@@ -2128,6 +2236,21 @@ void agent_on_message(const char* sender_pubkey_hex,
|
||||
break;
|
||||
}
|
||||
|
||||
uint64_t current_tool_fp = tool_calls_fingerprint(&resp);
|
||||
if (turn == 0 || current_tool_fp != last_tool_fp) {
|
||||
repeated_tool_turns = 1;
|
||||
last_tool_fp = current_tool_fp;
|
||||
} else {
|
||||
repeated_tool_turns++;
|
||||
}
|
||||
|
||||
if (repeated_tool_turns >= stall_repeat_threshold) {
|
||||
exited_on_stall = 1;
|
||||
stall_due_to_length = (resp.finish_reason && strcmp(resp.finish_reason, "length") == 0) ? 1 : 0;
|
||||
llm_response_free(&resp);
|
||||
break;
|
||||
}
|
||||
|
||||
if (append_assistant_tool_calls_message(messages, &resp) != 0) {
|
||||
llm_response_free(&resp);
|
||||
break;
|
||||
@@ -2162,10 +2285,44 @@ void agent_on_message(const char* sender_pubkey_hex,
|
||||
}
|
||||
|
||||
if (!final_answer_owned) {
|
||||
final_answer_owned = strdup("I hit my tool-use limit for this request.");
|
||||
char* messages_json = cJSON_PrintUnformatted(messages);
|
||||
if (messages_json) {
|
||||
llm_response_t final_resp;
|
||||
int final_rc = llm_chat_with_tools_messages(messages_json, tools_json, "none", &final_resp);
|
||||
free(messages_json);
|
||||
if (final_rc == 0) {
|
||||
const char* forced_answer = final_resp.content ? final_resp.content : "";
|
||||
if (forced_answer[0] != '\0') {
|
||||
final_answer_owned = strdup(forced_answer);
|
||||
}
|
||||
llm_response_free(&final_resp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int exhausted_on_max_turns = (!final_answer_owned && !exited_on_stall && turns_run >= max_turns);
|
||||
|
||||
if (!final_answer_owned) {
|
||||
final_answer_owned = strdup(exited_on_stall
|
||||
? "I stopped repeated tool calls after detecting a loop and could not produce a final summary."
|
||||
: "I hit my tool-use limit for this request.");
|
||||
}
|
||||
|
||||
const char* final_answer = final_answer_owned ? final_answer_owned : "I hit my tool-use limit for this request.";
|
||||
|
||||
if (exited_on_stall || exhausted_on_max_turns) {
|
||||
notify_admin_limit_diagnostic("dm_agent_loop",
|
||||
exited_on_stall ? "stall_repetition_detected" : "max_turns_exhausted",
|
||||
sender_pubkey_hex,
|
||||
max_turns,
|
||||
turns_run,
|
||||
stall_repeat_threshold,
|
||||
repeated_tool_turns,
|
||||
g_cfg->llm.max_tokens,
|
||||
(exited_on_stall && stall_due_to_length) ? "max_tokens_truncation" : NULL,
|
||||
(exited_on_stall && stall_due_to_length) ? "Increase max_tokens (example: /model_set {\"max_tokens\": 4096})" : NULL,
|
||||
final_answer);
|
||||
}
|
||||
fprintf(stdout, "[didactyl] final response: %.240s%s\n",
|
||||
final_answer,
|
||||
strlen(final_answer) > 240 ? "..." : "");
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
#include "config.h"
|
||||
#include "nostr_handler.h"
|
||||
#include "cjson/cJSON.h"
|
||||
#include "tools.h"
|
||||
#include "tools/tools.h"
|
||||
|
||||
struct trigger_manager;
|
||||
|
||||
|
||||
+12
-4
@@ -225,6 +225,7 @@ static int parse_tools_config(cJSON* root, didactyl_config_t* config) {
|
||||
cJSON* trigger_max_turns = cJSON_GetObjectItemCaseSensitive(tools, "trigger_max_turns");
|
||||
cJSON* api_default_max_turns = cJSON_GetObjectItemCaseSensitive(tools, "api_default_max_turns");
|
||||
cJSON* api_max_turns_ceiling = cJSON_GetObjectItemCaseSensitive(tools, "api_max_turns_ceiling");
|
||||
cJSON* stall_repeat_threshold = cJSON_GetObjectItemCaseSensitive(tools, "stall_repeat_threshold");
|
||||
cJSON* local_http_fetch_default_timeout_seconds =
|
||||
cJSON_GetObjectItemCaseSensitive(tools, "local_http_fetch_default_timeout_seconds");
|
||||
cJSON* local_http_fetch_max_timeout_seconds =
|
||||
@@ -244,6 +245,9 @@ static int parse_tools_config(cJSON* root, didactyl_config_t* config) {
|
||||
if (api_max_turns_ceiling && cJSON_IsNumber(api_max_turns_ceiling)) {
|
||||
config->tools.api_max_turns_ceiling = (int)api_max_turns_ceiling->valuedouble;
|
||||
}
|
||||
if (stall_repeat_threshold && cJSON_IsNumber(stall_repeat_threshold)) {
|
||||
config->tools.stall_repeat_threshold = (int)stall_repeat_threshold->valuedouble;
|
||||
}
|
||||
if (local_http_fetch_default_timeout_seconds && cJSON_IsNumber(local_http_fetch_default_timeout_seconds)) {
|
||||
config->tools.local_http_fetch_default_timeout_seconds =
|
||||
(int)local_http_fetch_default_timeout_seconds->valuedouble;
|
||||
@@ -294,6 +298,9 @@ static int parse_tools_config(cJSON* root, didactyl_config_t* config) {
|
||||
if (config->tools.api_default_max_turns > config->tools.api_max_turns_ceiling) {
|
||||
config->tools.api_default_max_turns = config->tools.api_max_turns_ceiling;
|
||||
}
|
||||
if (config->tools.stall_repeat_threshold < 2) {
|
||||
config->tools.stall_repeat_threshold = 3;
|
||||
}
|
||||
if (config->tools.local_http_fetch_default_timeout_seconds < 1) {
|
||||
config->tools.local_http_fetch_default_timeout_seconds = 20;
|
||||
}
|
||||
@@ -1124,10 +1131,11 @@ int config_load(const char* path, didactyl_config_t* config) {
|
||||
config->dm_protocol = DM_PROTOCOL_NIP04;
|
||||
|
||||
config->tools.enabled = 1;
|
||||
config->tools.max_turns = 8;
|
||||
config->tools.trigger_max_turns = 6;
|
||||
config->tools.api_default_max_turns = 4;
|
||||
config->tools.api_max_turns_ceiling = 16;
|
||||
config->tools.max_turns = 20;
|
||||
config->tools.trigger_max_turns = 12;
|
||||
config->tools.api_default_max_turns = 8;
|
||||
config->tools.api_max_turns_ceiling = 32;
|
||||
config->tools.stall_repeat_threshold = 3;
|
||||
config->tools.local_http_fetch_default_timeout_seconds = 20;
|
||||
config->tools.local_http_fetch_max_timeout_seconds = 120;
|
||||
config->tools.shell.enabled = 1;
|
||||
|
||||
@@ -50,6 +50,7 @@ typedef struct {
|
||||
int trigger_max_turns;
|
||||
int api_default_max_turns;
|
||||
int api_max_turns_ceiling;
|
||||
int stall_repeat_threshold;
|
||||
int local_http_fetch_default_timeout_seconds;
|
||||
int local_http_fetch_max_timeout_seconds;
|
||||
shell_tools_config_t shell;
|
||||
|
||||
+147
-5
@@ -5,6 +5,7 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
#include <time.h>
|
||||
|
||||
#include "agent.h"
|
||||
@@ -81,6 +82,82 @@ static int estimate_tokens_from_chars(int chars) {
|
||||
return (chars + 3) / 4;
|
||||
}
|
||||
|
||||
static uint64_t fnv1a64_local(const char* s) {
|
||||
uint64_t h = 1469598103934665603ULL;
|
||||
if (!s) return h;
|
||||
while (*s) {
|
||||
h ^= (unsigned char)(*s++);
|
||||
h *= 1099511628211ULL;
|
||||
}
|
||||
return h;
|
||||
}
|
||||
|
||||
static uint64_t tool_calls_fingerprint_local(const llm_response_t* resp) {
|
||||
if (!resp || resp->tool_call_count <= 0 || !resp->tool_calls) {
|
||||
return 1469598103934665603ULL;
|
||||
}
|
||||
|
||||
uint64_t h = 1469598103934665603ULL;
|
||||
for (int i = 0; i < resp->tool_call_count; i++) {
|
||||
const llm_tool_call_t* tc = &resp->tool_calls[i];
|
||||
uint64_t name_h = fnv1a64_local(tc->name ? tc->name : "");
|
||||
uint64_t args_h = fnv1a64_local(tc->arguments_json ? tc->arguments_json : "{}");
|
||||
uint64_t part = name_h ^ (args_h + 0x9e3779b97f4a7c15ULL + (name_h << 6) + (name_h >> 2));
|
||||
h ^= part + (uint64_t)(i + 1) * 1099511628211ULL;
|
||||
h *= 1099511628211ULL;
|
||||
}
|
||||
|
||||
h ^= (uint64_t)resp->tool_call_count;
|
||||
h *= 1099511628211ULL;
|
||||
return h;
|
||||
}
|
||||
|
||||
static void notify_admin_limit_diagnostic_http(const char* source,
|
||||
const char* reason,
|
||||
const char* subject,
|
||||
int max_turns,
|
||||
int turns_run,
|
||||
int stall_repeat_threshold,
|
||||
int repeated_tool_turns,
|
||||
int current_max_tokens,
|
||||
const char* possible_cause,
|
||||
const char* hint,
|
||||
const char* final_answer) {
|
||||
if (!g_api_ctx.cfg || g_api_ctx.cfg->admin.pubkey[0] == '\0') {
|
||||
return;
|
||||
}
|
||||
|
||||
char diag[1800];
|
||||
snprintf(diag,
|
||||
sizeof(diag),
|
||||
"⚠️ Didactyl limit diagnostic\n"
|
||||
"source=%s\n"
|
||||
"reason=%s\n"
|
||||
"subject=%s\n"
|
||||
"max_turns=%d\n"
|
||||
"turns_run=%d\n"
|
||||
"stall_repeat_threshold=%d\n"
|
||||
"repeated_tool_turns=%d\n"
|
||||
"current_max_tokens=%d\n"
|
||||
"possible_cause=%s\n"
|
||||
"hint=%s\n"
|
||||
"final_answer_preview=%.*s",
|
||||
source ? source : "unknown",
|
||||
reason ? reason : "unknown",
|
||||
subject ? subject : "n/a",
|
||||
max_turns,
|
||||
turns_run,
|
||||
stall_repeat_threshold,
|
||||
repeated_tool_turns,
|
||||
current_max_tokens,
|
||||
possible_cause ? possible_cause : "n/a",
|
||||
hint ? hint : "n/a",
|
||||
360,
|
||||
final_answer ? final_answer : "");
|
||||
|
||||
(void)nostr_handler_send_dm_auto(g_api_ctx.cfg->admin.pubkey, diag);
|
||||
}
|
||||
|
||||
static int uri_extract_after_prefix(const struct mg_str* uri,
|
||||
const char* prefix,
|
||||
char* out,
|
||||
@@ -649,10 +726,15 @@ static char* build_slash_help_all_json_local(void) {
|
||||
|
||||
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_content = adopted_root ? cJSON_GetObjectItemCaseSensitive(adopted_root, "content") : 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;
|
||||
const char* adoption_events_text = NULL;
|
||||
if (adoption_events_content && cJSON_IsString(adoption_events_content) && adoption_events_content->valuestring) {
|
||||
adoption_events_text = adoption_events_content->valuestring;
|
||||
} else if (adoption_events_json && cJSON_IsString(adoption_events_json) && adoption_events_json->valuestring) {
|
||||
adoption_events_text = adoption_events_json->valuestring;
|
||||
}
|
||||
cJSON* adoption_events = adoption_events_text ? cJSON_Parse(adoption_events_text) : NULL;
|
||||
|
||||
int adopted_count = 0;
|
||||
if (adoption_events && cJSON_IsArray(adoption_events) && cJSON_GetArraySize(adoption_events) > 0) {
|
||||
@@ -844,9 +926,20 @@ static cJSON* run_prompt_with_tools_convo(cJSON* convo,
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int stall_repeat_threshold = 3;
|
||||
if (g_api_ctx.cfg && g_api_ctx.cfg->tools.stall_repeat_threshold > 1) {
|
||||
stall_repeat_threshold = g_api_ctx.cfg->tools.stall_repeat_threshold;
|
||||
}
|
||||
|
||||
char* final_response = NULL;
|
||||
uint64_t last_tool_fp = 0;
|
||||
int repeated_tool_turns = 0;
|
||||
int exited_on_stall = 0;
|
||||
int stall_due_to_length = 0;
|
||||
int turns_run = 0;
|
||||
|
||||
for (int turn = 0; turn < max_turns; turn++) {
|
||||
turns_run = turn + 1;
|
||||
char* messages_json = cJSON_PrintUnformatted(convo);
|
||||
if (!messages_json) break;
|
||||
|
||||
@@ -877,6 +970,21 @@ static cJSON* run_prompt_with_tools_convo(cJSON* convo,
|
||||
break;
|
||||
}
|
||||
|
||||
uint64_t current_tool_fp = tool_calls_fingerprint_local(&resp);
|
||||
if (turn == 0 || current_tool_fp != last_tool_fp) {
|
||||
repeated_tool_turns = 1;
|
||||
last_tool_fp = current_tool_fp;
|
||||
} else {
|
||||
repeated_tool_turns++;
|
||||
}
|
||||
|
||||
if (repeated_tool_turns >= stall_repeat_threshold) {
|
||||
exited_on_stall = 1;
|
||||
stall_due_to_length = (resp.finish_reason && strcmp(resp.finish_reason, "length") == 0) ? 1 : 0;
|
||||
llm_response_free(&resp);
|
||||
break;
|
||||
}
|
||||
|
||||
if (append_assistant_tool_calls_message_local(convo, &resp) != 0) {
|
||||
llm_response_free(&resp);
|
||||
final_response = strdup("Failed to append assistant tool calls.");
|
||||
@@ -917,9 +1025,43 @@ static cJSON* run_prompt_with_tools_convo(cJSON* convo,
|
||||
llm_response_free(&resp);
|
||||
}
|
||||
|
||||
done:
|
||||
done:;
|
||||
int hit_max_turn_limit = (!final_response && !exited_on_stall && turns_run >= max_turns);
|
||||
|
||||
if (!final_response) {
|
||||
final_response = strdup(tool_limit_message ? tool_limit_message : "I hit my tool-use limit for this prompt run.");
|
||||
char* messages_json = cJSON_PrintUnformatted(convo);
|
||||
if (messages_json) {
|
||||
llm_response_t final_resp;
|
||||
int final_rc = llm_chat_with_tools_messages(messages_json, tools_json, "none", &final_resp);
|
||||
free(messages_json);
|
||||
if (final_rc == 0) {
|
||||
const char* forced_answer = final_resp.content ? final_resp.content : "";
|
||||
if (forced_answer[0] != '\0') {
|
||||
final_response = strdup(forced_answer);
|
||||
}
|
||||
llm_response_free(&final_resp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!final_response) {
|
||||
final_response = strdup(exited_on_stall
|
||||
? "Stopped repeated tool calls after detecting a loop, and no final summary was produced."
|
||||
: (tool_limit_message ? tool_limit_message : "I hit my tool-use limit for this prompt run."));
|
||||
}
|
||||
|
||||
if (exited_on_stall || hit_max_turn_limit) {
|
||||
notify_admin_limit_diagnostic_http("http_api_tool_loop",
|
||||
exited_on_stall ? "stall_repetition_detected" : "max_turns_exhausted",
|
||||
log_sender,
|
||||
max_turns,
|
||||
turns_run,
|
||||
stall_repeat_threshold,
|
||||
repeated_tool_turns,
|
||||
g_api_ctx.cfg ? g_api_ctx.cfg->llm.max_tokens : 0,
|
||||
(exited_on_stall && stall_due_to_length) ? "max_tokens_truncation" : NULL,
|
||||
(exited_on_stall && stall_due_to_length) ? "Increase max_tokens (example: /model_set {\"max_tokens\": 4096})" : NULL,
|
||||
final_response ? final_response : "");
|
||||
}
|
||||
|
||||
cJSON_AddBoolToObject(root, "success", 1);
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
#define DIDACTYL_HTTP_API_H
|
||||
|
||||
#include "config.h"
|
||||
#include "tools.h"
|
||||
#include "tools/tools.h"
|
||||
|
||||
struct trigger_manager;
|
||||
|
||||
|
||||
@@ -289,6 +289,11 @@ static int parse_llm_response(const char* json, llm_response_t* out) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
cJSON* finish_reason = first ? cJSON_GetObjectItemCaseSensitive(first, "finish_reason") : NULL;
|
||||
if (finish_reason && cJSON_IsString(finish_reason) && finish_reason->valuestring) {
|
||||
out->finish_reason = strdup(finish_reason->valuestring);
|
||||
}
|
||||
|
||||
cJSON* content = cJSON_GetObjectItemCaseSensitive(msg, "content");
|
||||
if (content && cJSON_IsString(content) && content->valuestring) {
|
||||
out->content = strdup(content->valuestring);
|
||||
@@ -459,6 +464,7 @@ int llm_chat_with_tools(const char* system_prompt,
|
||||
void llm_response_free(llm_response_t* response) {
|
||||
if (!response) return;
|
||||
free(response->content);
|
||||
free(response->finish_reason);
|
||||
for (int i = 0; i < response->tool_call_count; i++) {
|
||||
free(response->tool_calls[i].id);
|
||||
free(response->tool_calls[i].name);
|
||||
|
||||
@@ -13,6 +13,7 @@ typedef struct {
|
||||
char* content;
|
||||
llm_tool_call_t* tool_calls;
|
||||
int tool_call_count;
|
||||
char* finish_reason;
|
||||
} llm_response_t;
|
||||
|
||||
int llm_init(const llm_config_t* config);
|
||||
|
||||
+69
-33
@@ -75,6 +75,8 @@ static void print_usage(const char* prog) {
|
||||
" If file is missing, startup falls back to minimal nsec-only mode.\n"
|
||||
" --nsec <nsec_or_hex>\n"
|
||||
" Private key as nsec1... or 64-char hex; overrides DIDACTYL_NSEC.\n"
|
||||
" --admin <npub_or_hex>\n"
|
||||
" Administrator pubkey override; accepts npub1... or 64-char hex.\n"
|
||||
" --api-port <port>\n"
|
||||
" Enable HTTP API and bind to this port (1-65535).\n"
|
||||
" --api-bind <addr>\n"
|
||||
@@ -811,24 +813,29 @@ static int publish_encrypted_self_config(const didactyl_config_t* cfg, const cha
|
||||
return rc;
|
||||
}
|
||||
|
||||
static int persist_runtime_config_to_nostr(const didactyl_config_t* cfg) {
|
||||
static int persist_runtime_config_to_nostr(const didactyl_config_t* cfg, int persist_llm_config) {
|
||||
if (!cfg) return -1;
|
||||
|
||||
cJSON* llm = cJSON_CreateObject();
|
||||
if (!llm) return -1;
|
||||
cJSON_AddStringToObject(llm, "provider", cfg->llm.provider);
|
||||
cJSON_AddStringToObject(llm, "api_key", cfg->llm.api_key);
|
||||
cJSON_AddStringToObject(llm, "model", cfg->llm.model);
|
||||
cJSON_AddStringToObject(llm, "base_url", cfg->llm.base_url);
|
||||
cJSON_AddNumberToObject(llm, "max_tokens", cfg->llm.max_tokens);
|
||||
cJSON_AddNumberToObject(llm, "temperature", cfg->llm.temperature);
|
||||
int llm_rc = 0;
|
||||
if (persist_llm_config) {
|
||||
cJSON* llm = cJSON_CreateObject();
|
||||
if (!llm) return -1;
|
||||
cJSON_AddStringToObject(llm, "provider", cfg->llm.provider);
|
||||
cJSON_AddStringToObject(llm, "api_key", cfg->llm.api_key);
|
||||
cJSON_AddStringToObject(llm, "model", cfg->llm.model);
|
||||
cJSON_AddStringToObject(llm, "base_url", cfg->llm.base_url);
|
||||
cJSON_AddNumberToObject(llm, "max_tokens", cfg->llm.max_tokens);
|
||||
cJSON_AddNumberToObject(llm, "temperature", cfg->llm.temperature);
|
||||
|
||||
char* llm_json = cJSON_PrintUnformatted(llm);
|
||||
cJSON_Delete(llm);
|
||||
if (!llm_json) return -1;
|
||||
char* llm_json = cJSON_PrintUnformatted(llm);
|
||||
cJSON_Delete(llm);
|
||||
if (!llm_json) return -1;
|
||||
|
||||
int llm_rc = publish_encrypted_self_config(cfg, "llm_config", llm_json);
|
||||
free(llm_json);
|
||||
llm_rc = publish_encrypted_self_config(cfg, "llm_config", llm_json);
|
||||
free(llm_json);
|
||||
} else {
|
||||
DEBUG_INFO("[didactyl] startup phase: skipping llm_config publish because it was recalled from Nostr");
|
||||
}
|
||||
|
||||
cJSON* agent = cJSON_CreateObject();
|
||||
if (!agent) return llm_rc;
|
||||
@@ -844,15 +851,18 @@ static int persist_runtime_config_to_nostr(const didactyl_config_t* cfg) {
|
||||
return (llm_rc == 0 && agent_rc == 0) ? 0 : -1;
|
||||
}
|
||||
|
||||
static void recover_missing_runtime_config_from_nostr(didactyl_config_t* cfg) {
|
||||
static int recover_missing_runtime_config_from_nostr(didactyl_config_t* cfg) {
|
||||
if (!cfg) {
|
||||
return;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int llm_recalled_from_nostr = 0;
|
||||
|
||||
if (!llm_config_is_complete(&cfg->llm)) {
|
||||
char* llm_plaintext = NULL;
|
||||
if (fetch_self_config_plaintext(cfg, "llm_config", &llm_plaintext) == 0 && llm_plaintext) {
|
||||
if (apply_recalled_llm_config(cfg, llm_plaintext) == 0) {
|
||||
llm_recalled_from_nostr = 1;
|
||||
DEBUG_INFO("[didactyl] startup phase: recovered llm_config from Nostr");
|
||||
} else {
|
||||
DEBUG_WARN("[didactyl] startup phase: failed to apply recalled llm_config");
|
||||
@@ -863,24 +873,34 @@ static void recover_missing_runtime_config_from_nostr(didactyl_config_t* cfg) {
|
||||
free(llm_plaintext);
|
||||
}
|
||||
|
||||
if (!admin_config_is_complete(cfg)) {
|
||||
char* agent_plaintext = NULL;
|
||||
if (fetch_self_config_plaintext(cfg, "agent_config", &agent_plaintext) == 0 && agent_plaintext) {
|
||||
if (apply_recalled_agent_config(cfg, agent_plaintext) == 0) {
|
||||
char* agent_plaintext = NULL;
|
||||
if (fetch_self_config_plaintext(cfg, "agent_config", &agent_plaintext) == 0 && agent_plaintext) {
|
||||
didactyl_config_t recalled = *cfg;
|
||||
if (apply_recalled_agent_config(&recalled, agent_plaintext) == 0) {
|
||||
if (!admin_config_is_complete(cfg)) {
|
||||
snprintf(cfg->admin.pubkey, sizeof(cfg->admin.pubkey), "%s", recalled.admin.pubkey);
|
||||
cfg->dm_protocol = recalled.dm_protocol;
|
||||
DEBUG_INFO("[didactyl] startup phase: recovered agent_config from Nostr");
|
||||
} else {
|
||||
DEBUG_WARN("[didactyl] startup phase: failed to apply recalled agent_config");
|
||||
} else if (recalled.admin.pubkey[0] != '\0' && strcmp(cfg->admin.pubkey, recalled.admin.pubkey) != 0) {
|
||||
DEBUG_WARN("[didactyl] startup phase: agent_config admin pubkey differs from loaded config (loaded=%.16s..., nostr=%.16s...); loaded config wins",
|
||||
cfg->admin.pubkey,
|
||||
recalled.admin.pubkey);
|
||||
}
|
||||
} else {
|
||||
DEBUG_WARN("[didactyl] startup phase: agent_config recall unavailable");
|
||||
DEBUG_WARN("[didactyl] startup phase: failed to parse/decrypt recalled agent_config");
|
||||
}
|
||||
free(agent_plaintext);
|
||||
} else {
|
||||
DEBUG_WARN("[didactyl] startup phase: agent_config recall unavailable");
|
||||
}
|
||||
free(agent_plaintext);
|
||||
|
||||
return llm_recalled_from_nostr;
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
const char* config_path = "./genesis.jsonc";
|
||||
const char* cli_nsec = NULL;
|
||||
const char* cli_admin_pubkey = NULL;
|
||||
const char* api_bind_override = NULL;
|
||||
int api_port_override = 0;
|
||||
int debug_level = DEBUG_LEVEL_NONE;
|
||||
@@ -944,6 +964,8 @@ int main(int argc, char** argv) {
|
||||
debug_level = atoi(argv[++i]);
|
||||
} else if (strcmp(argv[i], "--nsec") == 0 && i + 1 < argc) {
|
||||
cli_nsec = argv[++i];
|
||||
} else if (strcmp(argv[i], "--admin") == 0 && i + 1 < argc) {
|
||||
cli_admin_pubkey = argv[++i];
|
||||
} else if (strcmp(argv[i], "--api-port") == 0 && i + 1 < argc) {
|
||||
api_port_override = atoi(argv[++i]);
|
||||
} else if (strcmp(argv[i], "--api-bind") == 0 && i + 1 < argc) {
|
||||
@@ -997,6 +1019,18 @@ int main(int argc, char** argv) {
|
||||
}
|
||||
}
|
||||
|
||||
if (cli_admin_pubkey && cli_admin_pubkey[0] != '\0') {
|
||||
char decoded_admin[65] = {0};
|
||||
if (decode_pubkey_hex_or_npub_local(cli_admin_pubkey, decoded_admin) != 0) {
|
||||
fprintf(stderr, "Invalid admin pubkey provided via --admin (expected npub1... or 64-char hex)\n");
|
||||
config_free(&cfg);
|
||||
nostr_cleanup();
|
||||
return 1;
|
||||
}
|
||||
snprintf(cfg.admin.pubkey, sizeof(cfg.admin.pubkey), "%s", decoded_admin);
|
||||
DEBUG_INFO("[didactyl] startup phase: admin override applied from --admin (%.16s...)", cfg.admin.pubkey);
|
||||
}
|
||||
|
||||
if (config_ensure_default_skill_startup_events(&cfg) != 0) {
|
||||
fprintf(stderr, "Failed to synthesize startup events from default_skill\n");
|
||||
config_free(&cfg);
|
||||
@@ -1105,8 +1139,8 @@ int main(int argc, char** argv) {
|
||||
}
|
||||
|
||||
startup_step_begin(2, "Recover runtime config from Nostr");
|
||||
recover_missing_runtime_config_from_nostr(&cfg);
|
||||
startup_step_ok(2, "Recover runtime config from Nostr", NULL);
|
||||
int llm_recalled_from_nostr = recover_missing_runtime_config_from_nostr(&cfg);
|
||||
startup_step_ok(2, "Recover runtime config from Nostr", llm_recalled_from_nostr ? "llm_config recalled" : NULL);
|
||||
|
||||
startup_step_begin(3, "Validate LLM config");
|
||||
if (!llm_config_is_complete(&cfg.llm)) {
|
||||
@@ -1124,7 +1158,7 @@ int main(int argc, char** argv) {
|
||||
if (!admin_config_is_complete(&cfg)) {
|
||||
startup_step_fail(4, "Validate admin config", "missing admin pubkey");
|
||||
fprintf(stderr,
|
||||
"Missing admin config: provide admin.pubkey in genesis or store d=agent_config via config_store\n");
|
||||
"Missing admin config: provide admin.pubkey in genesis, pass --admin, or store d=agent_config via config_store\n");
|
||||
nostr_handler_cleanup();
|
||||
config_free(&cfg);
|
||||
nostr_cleanup();
|
||||
@@ -1153,17 +1187,19 @@ int main(int argc, char** argv) {
|
||||
if (nostr_handler_reconcile_startup_events() != 0) {
|
||||
DEBUG_WARN("[didactyl] startup phase: reconcile startup events failed (continuing)");
|
||||
}
|
||||
if (persist_runtime_config_to_nostr(&cfg) != 0) {
|
||||
DEBUG_WARN("[didactyl] startup phase: failed to persist encrypted runtime config to Nostr");
|
||||
} else {
|
||||
DEBUG_INFO("[didactyl] startup phase: persisted encrypted runtime config to Nostr");
|
||||
}
|
||||
} else {
|
||||
DEBUG_INFO("[didactyl] startup phase: existing-agent mode; skipping bootstrap publish on subsequent run");
|
||||
DEBUG_INFO("[didactyl] startup phase: existing-agent mode; skipping startup-event reconcile on subsequent run");
|
||||
if (nostr_handler_load_system_context_from_adopted_skills() != 0) {
|
||||
DEBUG_WARN("[didactyl] startup phase: failed to load system context from adopted skills (continuing with fallback)");
|
||||
}
|
||||
}
|
||||
|
||||
if (persist_runtime_config_to_nostr(&cfg, llm_recalled_from_nostr ? 0 : 1) != 0) {
|
||||
DEBUG_WARN("[didactyl] startup phase: failed to persist encrypted runtime config to Nostr");
|
||||
} else {
|
||||
DEBUG_INFO("[didactyl] startup phase: persisted encrypted runtime config to Nostr");
|
||||
}
|
||||
|
||||
startup_step_ok(7, "Reconcile/persist startup state", NULL);
|
||||
|
||||
const char* system_context = nostr_handler_get_system_context();
|
||||
|
||||
+3
-3
@@ -11,9 +11,9 @@
|
||||
// Version information (auto-updated by build system)
|
||||
// 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 78
|
||||
#define DIDACTYL_VERSION "v0.0.78"
|
||||
#define DIDACTYL_VERSION_MINOR 1
|
||||
#define DIDACTYL_VERSION_PATCH 0
|
||||
#define DIDACTYL_VERSION "v0.1.0"
|
||||
|
||||
// Agent metadata
|
||||
#define DIDACTYL_NAME "Didactyl"
|
||||
|
||||
+89
-1
@@ -3802,6 +3802,55 @@ char* nostr_handler_get_admin_kind1_notes_context(void) {
|
||||
return out;
|
||||
}
|
||||
|
||||
static char* extract_latest_kind0_content_from_events_json(const char* events_json) {
|
||||
if (!events_json || events_json[0] == '\0') {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
cJSON* root = cJSON_Parse(events_json);
|
||||
if (!root || !cJSON_IsArray(root)) {
|
||||
cJSON_Delete(root);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
char* best_content = NULL;
|
||||
time_t best_created_at = 0;
|
||||
|
||||
int n = cJSON_GetArraySize(root);
|
||||
for (int i = 0; i < n; i++) {
|
||||
cJSON* ev = cJSON_GetArrayItem(root, i);
|
||||
if (!ev || !cJSON_IsObject(ev)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
cJSON* kind = cJSON_GetObjectItemCaseSensitive(ev, "kind");
|
||||
cJSON* content = cJSON_GetObjectItemCaseSensitive(ev, "content");
|
||||
cJSON* created_at = cJSON_GetObjectItemCaseSensitive(ev, "created_at");
|
||||
if (!kind || !cJSON_IsNumber(kind) || (int)kind->valuedouble != 0 ||
|
||||
!content || !cJSON_IsString(content) || !content->valuestring || content->valuestring[0] == '\0') {
|
||||
continue;
|
||||
}
|
||||
|
||||
time_t ev_created_at = 0;
|
||||
if (created_at && cJSON_IsNumber(created_at)) {
|
||||
ev_created_at = (time_t)created_at->valuedouble;
|
||||
}
|
||||
|
||||
if (!best_content || ev_created_at >= best_created_at) {
|
||||
char* dup = strdup(content->valuestring);
|
||||
if (!dup) {
|
||||
continue;
|
||||
}
|
||||
free(best_content);
|
||||
best_content = dup;
|
||||
best_created_at = ev_created_at;
|
||||
}
|
||||
}
|
||||
|
||||
cJSON_Delete(root);
|
||||
return best_content;
|
||||
}
|
||||
|
||||
char* nostr_handler_get_agent_kind0_context(void) {
|
||||
if (!g_cfg) {
|
||||
return NULL;
|
||||
@@ -3810,7 +3859,46 @@ char* nostr_handler_get_agent_kind0_context(void) {
|
||||
pthread_mutex_lock(&g_admin_ctx_mutex);
|
||||
char* out = g_agent_kind0_json ? strdup(g_agent_kind0_json) : NULL;
|
||||
pthread_mutex_unlock(&g_admin_ctx_mutex);
|
||||
return out;
|
||||
if (out && out[0] != '\0') {
|
||||
return out;
|
||||
}
|
||||
free(out);
|
||||
|
||||
cJSON* filter = cJSON_CreateObject();
|
||||
cJSON* kinds = cJSON_CreateArray();
|
||||
cJSON* authors = cJSON_CreateArray();
|
||||
if (!filter || !kinds || !authors) {
|
||||
cJSON_Delete(filter);
|
||||
cJSON_Delete(kinds);
|
||||
cJSON_Delete(authors);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
cJSON_AddItemToArray(kinds, cJSON_CreateNumber(0));
|
||||
cJSON_AddItemToObject(filter, "kinds", kinds);
|
||||
cJSON_AddItemToArray(authors, cJSON_CreateString(g_cfg->keys.public_key_hex));
|
||||
cJSON_AddItemToObject(filter, "authors", authors);
|
||||
cJSON_AddNumberToObject(filter, "limit", 8);
|
||||
|
||||
char* events_json = nostr_handler_query_json(filter, 3000);
|
||||
cJSON_Delete(filter);
|
||||
if (!events_json) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
char* fetched_kind0 = extract_latest_kind0_content_from_events_json(events_json);
|
||||
free(events_json);
|
||||
if (!fetched_kind0 || fetched_kind0[0] == '\0') {
|
||||
free(fetched_kind0);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
pthread_mutex_lock(&g_admin_ctx_mutex);
|
||||
free(g_agent_kind0_json);
|
||||
g_agent_kind0_json = strdup(fetched_kind0);
|
||||
pthread_mutex_unlock(&g_admin_ctx_mutex);
|
||||
|
||||
return fetched_kind0;
|
||||
}
|
||||
|
||||
char* nostr_handler_get_agent_kind3_context(void) {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#define DIDACTYL_PROMPT_TEMPLATE_H
|
||||
|
||||
#include "cjson/cJSON.h"
|
||||
#include "tools.h"
|
||||
#include "tools/tools.h"
|
||||
|
||||
#define PROMPT_TEMPLATE_MAX_SECTIONS 32
|
||||
#define PROMPT_TEMPLATE_MAX_NAME_LEN 64
|
||||
|
||||
+643
-15
@@ -271,10 +271,11 @@ static void config_set_defaults(didactyl_config_t* cfg) {
|
||||
cfg->dm_protocol = DM_PROTOCOL_NIP04;
|
||||
|
||||
cfg->tools.enabled = 1;
|
||||
cfg->tools.max_turns = 8;
|
||||
cfg->tools.trigger_max_turns = 6;
|
||||
cfg->tools.api_default_max_turns = 4;
|
||||
cfg->tools.api_max_turns_ceiling = 16;
|
||||
cfg->tools.max_turns = 20;
|
||||
cfg->tools.trigger_max_turns = 12;
|
||||
cfg->tools.api_default_max_turns = 8;
|
||||
cfg->tools.api_max_turns_ceiling = 32;
|
||||
cfg->tools.stall_repeat_threshold = 3;
|
||||
cfg->tools.local_http_fetch_default_timeout_seconds = 20;
|
||||
cfg->tools.local_http_fetch_max_timeout_seconds = 120;
|
||||
cfg->tools.shell.enabled = 1;
|
||||
@@ -801,6 +802,422 @@ static int recover_existing_config_from_nostr(didactyl_config_t* cfg, int* out_r
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int query_self_kind0_name(const didactyl_config_t* cfg,
|
||||
char* out_name,
|
||||
size_t out_name_size,
|
||||
int* out_found) {
|
||||
if (!cfg || !out_name || out_name_size == 0 || !out_found || cfg->keys.public_key_hex[0] == '\0') {
|
||||
return -1;
|
||||
}
|
||||
|
||||
out_name[0] = '\0';
|
||||
*out_found = 0;
|
||||
|
||||
cJSON* filter = cJSON_CreateObject();
|
||||
cJSON* kinds = cJSON_CreateArray();
|
||||
cJSON* authors = cJSON_CreateArray();
|
||||
if (!filter || !kinds || !authors) {
|
||||
cJSON_Delete(filter);
|
||||
cJSON_Delete(kinds);
|
||||
cJSON_Delete(authors);
|
||||
return -1;
|
||||
}
|
||||
|
||||
cJSON_AddItemToArray(kinds, cJSON_CreateNumber(0));
|
||||
cJSON_AddItemToObject(filter, "kinds", kinds);
|
||||
cJSON_AddItemToArray(authors, cJSON_CreateString(cfg->keys.public_key_hex));
|
||||
cJSON_AddItemToObject(filter, "authors", authors);
|
||||
cJSON_AddNumberToObject(filter, "limit", 1);
|
||||
|
||||
char* events_json = nostr_handler_query_json(filter, 3000);
|
||||
cJSON_Delete(filter);
|
||||
if (!events_json) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
cJSON* arr = cJSON_Parse(events_json);
|
||||
free(events_json);
|
||||
if (!arr || !cJSON_IsArray(arr) || cJSON_GetArraySize(arr) <= 0) {
|
||||
cJSON_Delete(arr);
|
||||
return 0;
|
||||
}
|
||||
|
||||
cJSON* ev = cJSON_GetArrayItem(arr, 0);
|
||||
cJSON* content = ev ? cJSON_GetObjectItemCaseSensitive(ev, "content") : NULL;
|
||||
if (!content || !cJSON_IsString(content) || !content->valuestring || content->valuestring[0] == '\0') {
|
||||
cJSON_Delete(arr);
|
||||
return 0;
|
||||
}
|
||||
|
||||
cJSON* profile = cJSON_Parse(content->valuestring);
|
||||
if (!profile || !cJSON_IsObject(profile)) {
|
||||
cJSON_Delete(profile);
|
||||
cJSON_Delete(arr);
|
||||
return 0;
|
||||
}
|
||||
|
||||
cJSON* display_name = cJSON_GetObjectItemCaseSensitive(profile, "display_name");
|
||||
cJSON* name = cJSON_GetObjectItemCaseSensitive(profile, "name");
|
||||
cJSON* chosen = NULL;
|
||||
|
||||
if (display_name && cJSON_IsString(display_name) && display_name->valuestring && display_name->valuestring[0] != '\0') {
|
||||
chosen = display_name;
|
||||
} else if (name && cJSON_IsString(name) && name->valuestring && name->valuestring[0] != '\0') {
|
||||
chosen = name;
|
||||
}
|
||||
|
||||
if (chosen && chosen->valuestring) {
|
||||
snprintf(out_name, out_name_size, "%s", chosen->valuestring);
|
||||
*out_found = 1;
|
||||
}
|
||||
|
||||
cJSON_Delete(profile);
|
||||
cJSON_Delete(arr);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int fetch_and_decrypt_self_config_wizard(const didactyl_config_t* cfg, const char* d_tag, char** out_plaintext) {
|
||||
if (!cfg || !d_tag || !out_plaintext || cfg->keys.public_key_hex[0] == '\0') {
|
||||
return -1;
|
||||
}
|
||||
|
||||
*out_plaintext = NULL;
|
||||
|
||||
cJSON* filter = cJSON_CreateObject();
|
||||
cJSON* kinds = cJSON_CreateArray();
|
||||
cJSON* authors = cJSON_CreateArray();
|
||||
cJSON* d_vals = cJSON_CreateArray();
|
||||
if (!filter || !kinds || !authors || !d_vals) {
|
||||
cJSON_Delete(filter);
|
||||
cJSON_Delete(kinds);
|
||||
cJSON_Delete(authors);
|
||||
cJSON_Delete(d_vals);
|
||||
return -1;
|
||||
}
|
||||
|
||||
cJSON_AddItemToArray(kinds, cJSON_CreateNumber(30078));
|
||||
cJSON_AddItemToObject(filter, "kinds", kinds);
|
||||
cJSON_AddItemToArray(authors, cJSON_CreateString(cfg->keys.public_key_hex));
|
||||
cJSON_AddItemToObject(filter, "authors", authors);
|
||||
cJSON_AddItemToArray(d_vals, cJSON_CreateString(d_tag));
|
||||
cJSON_AddItemToObject(filter, "#d", d_vals);
|
||||
cJSON_AddNumberToObject(filter, "limit", 1);
|
||||
|
||||
char* events_json = nostr_handler_query_json(filter, 4000);
|
||||
cJSON_Delete(filter);
|
||||
if (!events_json) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
cJSON* arr = cJSON_Parse(events_json);
|
||||
free(events_json);
|
||||
if (!arr || !cJSON_IsArray(arr) || cJSON_GetArraySize(arr) <= 0) {
|
||||
cJSON_Delete(arr);
|
||||
return -1;
|
||||
}
|
||||
|
||||
cJSON* ev = cJSON_GetArrayItem(arr, 0);
|
||||
cJSON* content = ev ? cJSON_GetObjectItemCaseSensitive(ev, "content") : NULL;
|
||||
if (!content || !cJSON_IsString(content) || !content->valuestring || content->valuestring[0] == '\0') {
|
||||
cJSON_Delete(arr);
|
||||
return -1;
|
||||
}
|
||||
|
||||
size_t out_cap = strlen(content->valuestring) + 1024U;
|
||||
char* plaintext = (char*)malloc(out_cap);
|
||||
if (!plaintext) {
|
||||
cJSON_Delete(arr);
|
||||
return -1;
|
||||
}
|
||||
|
||||
int dec_rc = nostr_nip44_decrypt(cfg->keys.private_key,
|
||||
cfg->keys.public_key,
|
||||
content->valuestring,
|
||||
plaintext,
|
||||
out_cap);
|
||||
cJSON_Delete(arr);
|
||||
if (dec_rc != NOSTR_SUCCESS) {
|
||||
free(plaintext);
|
||||
return -1;
|
||||
}
|
||||
|
||||
*out_plaintext = plaintext;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int apply_recalled_llm_config_wizard(didactyl_config_t* cfg, const char* plaintext) {
|
||||
if (!cfg || !plaintext) return -1;
|
||||
|
||||
cJSON* root = cJSON_Parse(plaintext);
|
||||
if (!root || !cJSON_IsObject(root)) {
|
||||
cJSON_Delete(root);
|
||||
return -1;
|
||||
}
|
||||
|
||||
cJSON* provider = cJSON_GetObjectItemCaseSensitive(root, "provider");
|
||||
cJSON* api_key = cJSON_GetObjectItemCaseSensitive(root, "api_key");
|
||||
cJSON* model = cJSON_GetObjectItemCaseSensitive(root, "model");
|
||||
cJSON* base_url = cJSON_GetObjectItemCaseSensitive(root, "base_url");
|
||||
cJSON* max_tokens = cJSON_GetObjectItemCaseSensitive(root, "max_tokens");
|
||||
cJSON* temperature = cJSON_GetObjectItemCaseSensitive(root, "temperature");
|
||||
|
||||
if (!api_key || !cJSON_IsString(api_key) || !api_key->valuestring || api_key->valuestring[0] == '\0' ||
|
||||
!model || !cJSON_IsString(model) || !model->valuestring || model->valuestring[0] == '\0' ||
|
||||
!base_url || !cJSON_IsString(base_url) || !base_url->valuestring || base_url->valuestring[0] == '\0') {
|
||||
cJSON_Delete(root);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (provider && cJSON_IsString(provider) && provider->valuestring && provider->valuestring[0] != '\0') {
|
||||
snprintf(cfg->llm.provider, sizeof(cfg->llm.provider), "%s", provider->valuestring);
|
||||
} else if (cfg->llm.provider[0] == '\0') {
|
||||
snprintf(cfg->llm.provider, sizeof(cfg->llm.provider), "%s", "openai");
|
||||
}
|
||||
|
||||
snprintf(cfg->llm.api_key, sizeof(cfg->llm.api_key), "%s", api_key->valuestring);
|
||||
snprintf(cfg->llm.model, sizeof(cfg->llm.model), "%s", model->valuestring);
|
||||
snprintf(cfg->llm.base_url, sizeof(cfg->llm.base_url), "%s", base_url->valuestring);
|
||||
if (max_tokens && cJSON_IsNumber(max_tokens)) {
|
||||
cfg->llm.max_tokens = (int)max_tokens->valuedouble;
|
||||
}
|
||||
if (temperature && cJSON_IsNumber(temperature)) {
|
||||
cfg->llm.temperature = temperature->valuedouble;
|
||||
}
|
||||
|
||||
cJSON_Delete(root);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int apply_recalled_agent_config_wizard(didactyl_config_t* cfg, const char* plaintext) {
|
||||
if (!cfg || !plaintext) return -1;
|
||||
|
||||
cJSON* root = cJSON_Parse(plaintext);
|
||||
if (!root || !cJSON_IsObject(root)) {
|
||||
cJSON_Delete(root);
|
||||
return -1;
|
||||
}
|
||||
|
||||
cJSON* admin_pubkey = cJSON_GetObjectItemCaseSensitive(root, "admin_pubkey");
|
||||
cJSON* dm_protocol = cJSON_GetObjectItemCaseSensitive(root, "dm_protocol");
|
||||
|
||||
if (admin_pubkey && cJSON_IsString(admin_pubkey) && admin_pubkey->valuestring && admin_pubkey->valuestring[0] != '\0') {
|
||||
char decoded[65] = {0};
|
||||
if (decode_pubkey_hex_or_npub_local(admin_pubkey->valuestring, decoded) == 0) {
|
||||
snprintf(cfg->admin.pubkey, sizeof(cfg->admin.pubkey), "%s", decoded);
|
||||
}
|
||||
}
|
||||
|
||||
if (dm_protocol && cJSON_IsString(dm_protocol) && dm_protocol->valuestring && dm_protocol->valuestring[0] != '\0') {
|
||||
if (strcmp(dm_protocol->valuestring, "nip17") == 0) {
|
||||
cfg->dm_protocol = DM_PROTOCOL_NIP17;
|
||||
} else if (strcmp(dm_protocol->valuestring, "both") == 0) {
|
||||
cfg->dm_protocol = DM_PROTOCOL_BOTH;
|
||||
} else {
|
||||
cfg->dm_protocol = DM_PROTOCOL_NIP04;
|
||||
}
|
||||
}
|
||||
|
||||
cJSON_Delete(root);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int recover_full_config_from_nostr(didactyl_config_t* cfg,
|
||||
char* out_agent_name,
|
||||
size_t out_agent_name_size,
|
||||
int* out_agent_name_found) {
|
||||
if (!cfg || !out_agent_name || out_agent_name_size == 0 || !out_agent_name_found) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
out_agent_name[0] = '\0';
|
||||
*out_agent_name_found = 0;
|
||||
|
||||
didactyl_config_t tmp = *cfg;
|
||||
if (nostr_handler_init(&tmp) != 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
(void)wait_connected_relays_ms(5000);
|
||||
|
||||
(void)query_self_kind0_name(cfg, out_agent_name, out_agent_name_size, out_agent_name_found);
|
||||
|
||||
char* llm_plaintext = NULL;
|
||||
if (fetch_and_decrypt_self_config_wizard(cfg, "llm_config", &llm_plaintext) == 0 && llm_plaintext) {
|
||||
(void)apply_recalled_llm_config_wizard(cfg, llm_plaintext);
|
||||
}
|
||||
free(llm_plaintext);
|
||||
|
||||
char* agent_plaintext = NULL;
|
||||
if (fetch_and_decrypt_self_config_wizard(cfg, "agent_config", &agent_plaintext) == 0 && agent_plaintext) {
|
||||
(void)apply_recalled_agent_config_wizard(cfg, agent_plaintext);
|
||||
}
|
||||
free(agent_plaintext);
|
||||
|
||||
nostr_handler_cleanup();
|
||||
return 0;
|
||||
}
|
||||
|
||||
static const char* dm_protocol_to_string_local(dm_protocol_t protocol) {
|
||||
switch (protocol) {
|
||||
case DM_PROTOCOL_NIP17: return "nip17";
|
||||
case DM_PROTOCOL_BOTH: return "both";
|
||||
case DM_PROTOCOL_NIP04:
|
||||
default:
|
||||
return "nip04";
|
||||
}
|
||||
}
|
||||
|
||||
static int publish_encrypted_self_config_wizard(const didactyl_config_t* cfg,
|
||||
const char* d_tag,
|
||||
const char* content_json) {
|
||||
if (!cfg || !d_tag || !content_json || d_tag[0] == '\0') return -1;
|
||||
|
||||
size_t cipher_cap = (strlen(content_json) * 4U) + 1024U;
|
||||
char* ciphertext = (char*)malloc(cipher_cap);
|
||||
if (!ciphertext) return -1;
|
||||
|
||||
int enc_rc = nostr_nip44_encrypt(cfg->keys.private_key,
|
||||
cfg->keys.public_key,
|
||||
content_json,
|
||||
ciphertext,
|
||||
cipher_cap);
|
||||
if (enc_rc != NOSTR_SUCCESS) {
|
||||
free(ciphertext);
|
||||
return -1;
|
||||
}
|
||||
|
||||
cJSON* tags = cJSON_CreateArray();
|
||||
cJSON* d = cJSON_CreateArray();
|
||||
cJSON* app = cJSON_CreateArray();
|
||||
if (!tags || !d || !app) {
|
||||
cJSON_Delete(tags);
|
||||
cJSON_Delete(d);
|
||||
cJSON_Delete(app);
|
||||
free(ciphertext);
|
||||
return -1;
|
||||
}
|
||||
|
||||
cJSON_AddItemToArray(d, cJSON_CreateString("d"));
|
||||
cJSON_AddItemToArray(d, cJSON_CreateString(d_tag));
|
||||
cJSON_AddItemToArray(tags, d);
|
||||
|
||||
cJSON_AddItemToArray(app, cJSON_CreateString("app"));
|
||||
cJSON_AddItemToArray(app, cJSON_CreateString("didactyl"));
|
||||
cJSON_AddItemToArray(tags, app);
|
||||
|
||||
nostr_publish_result_t result;
|
||||
memset(&result, 0, sizeof(result));
|
||||
int rc = nostr_handler_publish_kind_event(30078, ciphertext, tags, &result);
|
||||
nostr_handler_publish_result_free(&result);
|
||||
cJSON_Delete(tags);
|
||||
free(ciphertext);
|
||||
return rc;
|
||||
}
|
||||
|
||||
static int persist_runtime_config_to_nostr_wizard(const didactyl_config_t* cfg) {
|
||||
if (!cfg) return -1;
|
||||
|
||||
cJSON* llm = cJSON_CreateObject();
|
||||
if (!llm) return -1;
|
||||
cJSON_AddStringToObject(llm, "provider", cfg->llm.provider);
|
||||
cJSON_AddStringToObject(llm, "api_key", cfg->llm.api_key);
|
||||
cJSON_AddStringToObject(llm, "model", cfg->llm.model);
|
||||
cJSON_AddStringToObject(llm, "base_url", cfg->llm.base_url);
|
||||
cJSON_AddNumberToObject(llm, "max_tokens", cfg->llm.max_tokens);
|
||||
cJSON_AddNumberToObject(llm, "temperature", cfg->llm.temperature);
|
||||
|
||||
char* llm_json = cJSON_PrintUnformatted(llm);
|
||||
cJSON_Delete(llm);
|
||||
if (!llm_json) return -1;
|
||||
|
||||
int llm_rc = publish_encrypted_self_config_wizard(cfg, "llm_config", llm_json);
|
||||
free(llm_json);
|
||||
|
||||
cJSON* agent = cJSON_CreateObject();
|
||||
if (!agent) return llm_rc;
|
||||
cJSON_AddStringToObject(agent, "admin_pubkey", cfg->admin.pubkey);
|
||||
cJSON_AddStringToObject(agent, "dm_protocol", dm_protocol_to_string_local(cfg->dm_protocol));
|
||||
char* agent_json = cJSON_PrintUnformatted(agent);
|
||||
cJSON_Delete(agent);
|
||||
if (!agent_json) return llm_rc;
|
||||
|
||||
int agent_rc = publish_encrypted_self_config_wizard(cfg, "agent_config", agent_json);
|
||||
free(agent_json);
|
||||
|
||||
return (llm_rc == 0 && agent_rc == 0) ? 0 : -1;
|
||||
}
|
||||
|
||||
static int persist_runtime_config_to_nostr_wizard_online(const didactyl_config_t* cfg) {
|
||||
if (!cfg) return -1;
|
||||
|
||||
didactyl_config_t tmp = *cfg;
|
||||
if (nostr_handler_init(&tmp) != 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
(void)wait_connected_relays_ms(5000);
|
||||
int rc = persist_runtime_config_to_nostr_wizard(cfg);
|
||||
|
||||
/* Give relays time to acknowledge the published events before tearing
|
||||
down the connection. Without this, the fire-and-forget publish may
|
||||
not reach any relay before cleanup closes the sockets. */
|
||||
for (int i = 0; i < 30; i++) {
|
||||
nostr_handler_poll(100);
|
||||
}
|
||||
|
||||
nostr_handler_cleanup();
|
||||
return rc;
|
||||
}
|
||||
|
||||
static int publish_kind10002_relays_wizard_online(const didactyl_config_t* cfg) {
|
||||
if (!cfg) return -1;
|
||||
|
||||
didactyl_config_t tmp = *cfg;
|
||||
if (nostr_handler_init(&tmp) != 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
(void)wait_connected_relays_ms(5000);
|
||||
|
||||
char* tags_json = NULL;
|
||||
if (build_kind10002_tags_json(cfg, &tags_json) != 0 || !tags_json) {
|
||||
free(tags_json);
|
||||
nostr_handler_cleanup();
|
||||
return -1;
|
||||
}
|
||||
|
||||
cJSON* tags = cJSON_Parse(tags_json);
|
||||
free(tags_json);
|
||||
if (!tags || !cJSON_IsArray(tags)) {
|
||||
cJSON_Delete(tags);
|
||||
nostr_handler_cleanup();
|
||||
return -1;
|
||||
}
|
||||
|
||||
nostr_publish_result_t result;
|
||||
memset(&result, 0, sizeof(result));
|
||||
int rc = nostr_handler_publish_kind_event(10002, "", tags, &result);
|
||||
nostr_handler_publish_result_free(&result);
|
||||
cJSON_Delete(tags);
|
||||
|
||||
nostr_handler_cleanup();
|
||||
return rc;
|
||||
}
|
||||
|
||||
static int ensure_startup_kind10002_from_current_relays(didactyl_config_t* cfg) {
|
||||
if (!cfg) return -1;
|
||||
|
||||
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, 10002, "", tags_json);
|
||||
free(tags_json);
|
||||
return rc;
|
||||
}
|
||||
|
||||
static int wizard_llm_test(const llm_config_t* llm_cfg) {
|
||||
if (!llm_cfg) return -1;
|
||||
if (llm_init(llm_cfg) != 0) {
|
||||
@@ -1037,10 +1454,12 @@ static int prompt_model_selection(const cJSON* models,
|
||||
}
|
||||
}
|
||||
|
||||
static int prompt_admin_pubkey(didactyl_config_t* cfg) {
|
||||
static int prompt_admin_pubkey_with_header(didactyl_config_t* cfg,
|
||||
const char* step_title,
|
||||
const char* section_title) {
|
||||
char input[WIZARD_LINE_MAX];
|
||||
for (;;) {
|
||||
render_wizard_page_header("Step 3 of 7", "New Agent Setup -- Administrator");
|
||||
render_wizard_page_header(step_title, section_title);
|
||||
if (read_line_prompt(" Enter the admin's Nostr public key (npub1... or hex):\n> ", input, sizeof(input)) != 0) {
|
||||
return -1;
|
||||
}
|
||||
@@ -1056,9 +1475,15 @@ static int prompt_admin_pubkey(didactyl_config_t* cfg) {
|
||||
}
|
||||
}
|
||||
|
||||
static int prompt_llm_config(didactyl_config_t* cfg) {
|
||||
static int prompt_admin_pubkey(didactyl_config_t* cfg) {
|
||||
return prompt_admin_pubkey_with_header(cfg, "Step 3 of 7", "New Agent Setup -- Administrator");
|
||||
}
|
||||
|
||||
static int prompt_llm_config_with_header(didactyl_config_t* cfg,
|
||||
const char* step_title,
|
||||
const char* section_title) {
|
||||
for (;;) {
|
||||
render_wizard_page_header("Step 5 of 7", "New Agent Setup -- LLM Provider");
|
||||
render_wizard_page_header(step_title, section_title);
|
||||
print_option('1', " ppq.ai");
|
||||
print_option('2', " Routstr");
|
||||
print_option('3', " OpenAI-compatible");
|
||||
@@ -1179,11 +1604,17 @@ static int prompt_llm_config(didactyl_config_t* cfg) {
|
||||
}
|
||||
}
|
||||
|
||||
static int prompt_relay_configuration(didactyl_config_t* cfg) {
|
||||
static int prompt_llm_config(didactyl_config_t* cfg) {
|
||||
return prompt_llm_config_with_header(cfg, "Step 5 of 7", "New Agent Setup -- LLM Provider");
|
||||
}
|
||||
|
||||
static int prompt_relay_configuration_with_header(didactyl_config_t* cfg,
|
||||
const char* step_title,
|
||||
const char* section_title) {
|
||||
char input[WIZARD_LINE_MAX];
|
||||
|
||||
for (;;) {
|
||||
render_wizard_page_header("Step 6 of 7", "New Agent Setup -- Relay Configuration");
|
||||
render_wizard_page_header(step_title, section_title);
|
||||
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] : "");
|
||||
@@ -1235,6 +1666,10 @@ static int prompt_relay_configuration(didactyl_config_t* cfg) {
|
||||
}
|
||||
}
|
||||
|
||||
static int prompt_relay_configuration(didactyl_config_t* cfg) {
|
||||
return prompt_relay_configuration_with_header(cfg, "Step 6 of 7", "New Agent Setup -- Relay Configuration");
|
||||
}
|
||||
|
||||
static int run_command_local(char* const argv[]) {
|
||||
pid_t pid = fork();
|
||||
if (pid < 0) return -1;
|
||||
@@ -1749,6 +2184,13 @@ static int new_agent_flow(didactyl_config_t* cfg, char* genesis_path_out, size_t
|
||||
if (c == 's') return 2;
|
||||
|
||||
if (c == 'b') {
|
||||
/* Publish kind 30078 agent_config + llm_config so the --nsec-only
|
||||
restart path can recover them. Non-fatal: main() will also
|
||||
publish during bootstrap, but doing it here is belt-and-suspenders. */
|
||||
if (persist_runtime_config_to_nostr_wizard_online(cfg) != 0) {
|
||||
fprintf(stderr, "%sWarning: failed to publish runtime config to Nostr (will retry on boot).%s\n",
|
||||
ANSI_YELLOW, ANSI_RESET);
|
||||
}
|
||||
fprintf(stderr, "%sStep 7 of 7 -- Booting new agent. Check your messages for initial greeting.%s\n",
|
||||
ANSI_YELLOW,
|
||||
ANSI_RESET);
|
||||
@@ -1756,6 +2198,16 @@ static int new_agent_flow(didactyl_config_t* cfg, char* genesis_path_out, size_t
|
||||
}
|
||||
|
||||
if (c == 'i') {
|
||||
/* Publish kind 30078 agent_config + llm_config BEFORE installing the
|
||||
service. The systemd service starts with --nsec only and depends
|
||||
on recovering these from relays. */
|
||||
if (persist_runtime_config_to_nostr_wizard_online(cfg) != 0) {
|
||||
fprintf(stderr,
|
||||
"%sFailed to publish runtime config to Nostr; refusing install to avoid missing admin/LLM on first boot.%s\n",
|
||||
ANSI_RED, ANSI_RESET);
|
||||
return -1;
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -1794,6 +2246,10 @@ static int new_agent_flow(didactyl_config_t* cfg, char* genesis_path_out, size_t
|
||||
|
||||
static int existing_agent_flow(didactyl_config_t* cfg) {
|
||||
char nsec_in[OW_MAX_KEY_LEN] = {0};
|
||||
char agent_name[OW_MAX_NAME_LEN] = {0};
|
||||
int agent_name_found = 0;
|
||||
int relay_changed = 0;
|
||||
|
||||
render_wizard_page_header("Existing Agent", "Identity");
|
||||
if (read_secret_prompt("Enter your agent nsec (nsec1... or 64-char hex): ", nsec_in, sizeof(nsec_in)) != 0) return -1;
|
||||
if (line_is_quit(nsec_in)) return -1;
|
||||
@@ -1807,13 +2263,183 @@ static int existing_agent_flow(didactyl_config_t* cfg) {
|
||||
render_wizard_page_header("Existing Agent", "Config Recovery");
|
||||
fprintf(stderr, " Relay list (kind 10002): NOT FOUND\n\n");
|
||||
fprintf(stderr, "%sUnable to find a kind 10002 relay list for this identity.%s\n", ANSI_RED, ANSI_RESET);
|
||||
fprintf(stderr, "\n");
|
||||
print_option('n', "ew agent setup with this identity");
|
||||
print_option('q', "uit to main menu");
|
||||
wizard_option_t opts[] = {{'n', ""}, {'q', ""}};
|
||||
char c = read_menu_choice(opts, 2);
|
||||
if (c == 'n') {
|
||||
int rc = new_agent_flow(cfg, NULL, 0);
|
||||
if (rc == 0) return SETUP_WIZARD_RC_BOOTSTRAP;
|
||||
if (rc == 1) return SETUP_WIZARD_RC_EXIT;
|
||||
return -1;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
render_wizard_page_header("Existing Agent", "Config Recovery");
|
||||
fprintf(stderr, " Relay list (kind 10002): FOUND (%d relays)\n\n", cfg->relay_count);
|
||||
if (recover_full_config_from_nostr(cfg, agent_name, sizeof(agent_name), &agent_name_found) != 0) {
|
||||
fprintf(stderr, "%sWarning: unable to recover full config from Nostr, continuing with partial recovery.%s\n",
|
||||
ANSI_YELLOW,
|
||||
ANSI_RESET);
|
||||
}
|
||||
|
||||
return 0;
|
||||
if (!agent_name_found || agent_name[0] == '\0') {
|
||||
snprintf(agent_name, sizeof(agent_name), "%s", "Didactyl");
|
||||
}
|
||||
|
||||
for (;;) {
|
||||
char masked_key[64] = {0};
|
||||
size_t api_len = strlen(cfg->llm.api_key);
|
||||
if (api_len >= 8U) {
|
||||
snprintf(masked_key,
|
||||
sizeof(masked_key),
|
||||
"%.4s...%s",
|
||||
cfg->llm.api_key,
|
||||
cfg->llm.api_key + api_len - 4U);
|
||||
} else if (api_len > 0U) {
|
||||
snprintf(masked_key, sizeof(masked_key), "****");
|
||||
} else {
|
||||
snprintf(masked_key, sizeof(masked_key), "(not set)");
|
||||
}
|
||||
|
||||
render_wizard_page_header("Existing Agent", "Recovered Configuration");
|
||||
fprintf(stderr, "Your agent was found. Change any of the following:\n\n");
|
||||
fprintf(stderr, " Name: %s\n", agent_name);
|
||||
fprintf(stderr, " Identity: %.16s...\n", cfg->keys.public_key_hex);
|
||||
fprintf(stderr, " Admin: %s\n", cfg->admin.pubkey[0] ? cfg->admin.pubkey : "(not set)");
|
||||
fprintf(stderr, " LLM Provider: %s\n", cfg->llm.provider[0] ? cfg->llm.provider : "(not set)");
|
||||
fprintf(stderr, " LLM Model: %s\n", cfg->llm.model[0] ? cfg->llm.model : "(not set)");
|
||||
fprintf(stderr, " LLM Base URL: %s\n", cfg->llm.base_url[0] ? cfg->llm.base_url : "(not set)");
|
||||
fprintf(stderr, " LLM API Key: %s\n", masked_key);
|
||||
fprintf(stderr, " Relays: %d configured\n", cfg->relay_count);
|
||||
fprintf(stderr, "\n");
|
||||
|
||||
print_option('a', "dmin pubkey");
|
||||
print_option('l', "lm config");
|
||||
print_option('r', "elay configuration");
|
||||
print_option('c', "ontinue to launch options");
|
||||
print_option('q', "uit");
|
||||
|
||||
wizard_option_t opts[] = {{'a', ""}, {'l', ""}, {'r', ""}, {'c', ""}, {'q', ""}};
|
||||
char c = read_menu_choice(opts, 5);
|
||||
if (c == 'q') {
|
||||
return -1;
|
||||
}
|
||||
if (c == 'c') {
|
||||
break;
|
||||
}
|
||||
if (c == 'a') {
|
||||
if (prompt_admin_pubkey_with_header(cfg,
|
||||
"Existing Agent -- Review",
|
||||
"Existing Agent -- Administrator") != 0) {
|
||||
return -1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (c == 'l') {
|
||||
if (prompt_llm_config_with_header(cfg,
|
||||
"Existing Agent -- Review",
|
||||
"Existing Agent -- LLM Provider") != 0) {
|
||||
return -1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (c == 'r') {
|
||||
for (;;) {
|
||||
int rc = prompt_relay_configuration_with_header(cfg,
|
||||
"Existing Agent -- Review",
|
||||
"Existing Agent -- Relay Configuration");
|
||||
if (rc < 0) return -1;
|
||||
if (rc == 1) break;
|
||||
relay_changed = 1;
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
render_wizard_page_header("Existing Agent", "Launch Options");
|
||||
fprintf(stderr, " Name: %s\n", agent_name);
|
||||
fprintf(stderr, " Identity: %.16s...\n", cfg->keys.public_key_hex);
|
||||
fprintf(stderr, " Admin: %.16s...\n", cfg->admin.pubkey);
|
||||
fprintf(stderr, " LLM: %s @ %s\n", cfg->llm.model, cfg->llm.base_url);
|
||||
fprintf(stderr, " Relays: %d configured\n\n", cfg->relay_count);
|
||||
|
||||
print_option('b', "oot the agent now");
|
||||
print_option('i', "nstall dedicated-user systemd service and boot");
|
||||
print_option('q', "uit");
|
||||
|
||||
wizard_option_t launch_opts[] = {{'b', ""}, {'i', ""}, {'q', ""}};
|
||||
char lc = read_menu_choice(launch_opts, 3);
|
||||
if (lc == 'q') return -1;
|
||||
|
||||
if (lc == 'b') {
|
||||
if (relay_changed) {
|
||||
if (ensure_startup_kind10002_from_current_relays(cfg) != 0) {
|
||||
fprintf(stderr,
|
||||
"%sFailed to stage updated relay list for startup publish.%s\n",
|
||||
ANSI_RED,
|
||||
ANSI_RESET);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
/* Always return BOOTSTRAP so main() re-publishes kind 30078.
|
||||
The wizard has the authoritative config in memory. */
|
||||
return SETUP_WIZARD_RC_BOOTSTRAP;
|
||||
}
|
||||
|
||||
if (lc == 'i') {
|
||||
/* Always publish kind 30078 before installing the service, even if
|
||||
config was not changed. The service depends on recovering these
|
||||
from relays and the event may have been purged. */
|
||||
if (persist_runtime_config_to_nostr_wizard_online(cfg) != 0) {
|
||||
fprintf(stderr,
|
||||
"%sFailed to publish runtime config to Nostr; refusing install to avoid stale admin/LLM on first boot.%s\n",
|
||||
ANSI_RED,
|
||||
ANSI_RESET);
|
||||
return -1;
|
||||
}
|
||||
if (relay_changed) {
|
||||
if (publish_kind10002_relays_wizard_online(cfg) != 0) {
|
||||
fprintf(stderr,
|
||||
"%sFailed to publish updated relay list (kind 10002) to Nostr; refusing install.%s\n",
|
||||
ANSI_RED,
|
||||
ANSI_RESET);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
char service_user[64] = {0};
|
||||
sanitize_service_user_from_name(agent_name, service_user, sizeof(service_user));
|
||||
|
||||
char service_name[96] = {0};
|
||||
snprintf(service_name, sizeof(service_name), "%s.service", service_user);
|
||||
|
||||
sleep(2);
|
||||
char* is_active_argv[] = {"systemctl", "is-active", "--quiet", service_name, NULL};
|
||||
if (run_privileged_command_local(is_active_argv) == 0) {
|
||||
fprintf(stderr,
|
||||
"%sService %s is active. Setup is complete; this wizard will now exit.%s\n",
|
||||
ANSI_YELLOW,
|
||||
service_name,
|
||||
ANSI_RESET);
|
||||
} else {
|
||||
fprintf(stderr,
|
||||
"%sService installed but not active yet. Check status with: sudo systemctl status %s%s\n",
|
||||
ANSI_RED,
|
||||
service_name,
|
||||
ANSI_RESET);
|
||||
}
|
||||
|
||||
return SETUP_WIZARD_RC_EXIT;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int load_genesis_path_flow(didactyl_config_t* cfg) {
|
||||
@@ -1876,8 +2502,10 @@ int setup_wizard_run(didactyl_config_t* config, char* genesis_path_out, size_t g
|
||||
|
||||
if (c == 'e') {
|
||||
int rc = existing_agent_flow(config);
|
||||
if (rc == 0) {
|
||||
return SETUP_WIZARD_RC_EXISTING;
|
||||
if (rc == SETUP_WIZARD_RC_BOOTSTRAP ||
|
||||
rc == SETUP_WIZARD_RC_EXISTING ||
|
||||
rc == SETUP_WIZARD_RC_EXIT) {
|
||||
return rc;
|
||||
}
|
||||
return rc;
|
||||
}
|
||||
|
||||
@@ -47,8 +47,6 @@ char* execute_admin_identity(tools_context_t* ctx, const char* args_json) {
|
||||
}
|
||||
|
||||
cJSON_AddBoolToObject(out, "success", 1);
|
||||
cJSON_AddStringToObject(out, "admin_pubkey", ctx->cfg->admin.pubkey[0] ? ctx->cfg->admin.pubkey : "unknown");
|
||||
cJSON_AddStringToObject(out, "sender_verification", tier_text);
|
||||
|
||||
char content[768];
|
||||
snprintf(content,
|
||||
@@ -82,7 +80,6 @@ char* execute_nostr_admin_profile(tools_context_t* ctx, const char* args_json) {
|
||||
}
|
||||
|
||||
cJSON_AddBoolToObject(out, "success", 1);
|
||||
cJSON_AddStringToObject(out, "admin_kind0_json", profile_json);
|
||||
|
||||
size_t content_len = strlen("## Administrator Kind 0 Profile (source: nostr kind 0)\n\nAdministrator kind 0 profile content (JSON): ") + strlen(profile_json) + 1U;
|
||||
char* content = (char*)malloc(content_len);
|
||||
@@ -121,7 +118,6 @@ char* execute_nostr_admin_contacts(tools_context_t* ctx, const char* args_json)
|
||||
}
|
||||
|
||||
cJSON_AddBoolToObject(out, "success", 1);
|
||||
cJSON_AddStringToObject(out, "admin_kind3_json", contacts_json);
|
||||
|
||||
size_t content_len = strlen("## Administrator Kind 3 Contacts (source: nostr kind 3)\n\nAdministrator contacts (JSON): ") + strlen(contacts_json) + 1U;
|
||||
char* content = (char*)malloc(content_len);
|
||||
@@ -160,7 +156,6 @@ char* execute_nostr_admin_relays(tools_context_t* ctx, const char* args_json) {
|
||||
}
|
||||
|
||||
cJSON_AddBoolToObject(out, "success", 1);
|
||||
cJSON_AddStringToObject(out, "admin_kind10002_json", relays_json);
|
||||
|
||||
size_t content_len = strlen("## Administrator Kind 10002 Relays (source: nostr kind 10002)\n\nAdministrator relay list (JSON): ") + strlen(relays_json) + 1U;
|
||||
char* content = (char*)malloc(content_len);
|
||||
@@ -199,7 +194,6 @@ char* execute_nostr_admin_notes(tools_context_t* ctx, const char* args_json) {
|
||||
}
|
||||
|
||||
cJSON_AddBoolToObject(out, "success", 1);
|
||||
cJSON_AddStringToObject(out, "admin_notes_content", notes_text);
|
||||
|
||||
size_t content_len = strlen("## Administrator Recent Kind 1 Notes\n\n") + strlen(notes_text) + 1U;
|
||||
char* content = (char*)malloc(content_len);
|
||||
|
||||
@@ -62,8 +62,6 @@ char* execute_agent_identity(tools_context_t* ctx, const char* args_json) {
|
||||
cJSON* out = cJSON_CreateObject();
|
||||
if (!out) return NULL;
|
||||
cJSON_AddBoolToObject(out, "success", 1);
|
||||
cJSON_AddStringToObject(out, "agent_pubkey", ctx->cfg->keys.public_key_hex[0] ? ctx->cfg->keys.public_key_hex : "unknown");
|
||||
cJSON_AddStringToObject(out, "agent_npub", npub);
|
||||
|
||||
char content[384];
|
||||
snprintf(content,
|
||||
@@ -95,7 +93,6 @@ char* execute_nostr_agent_profile(tools_context_t* ctx, const char* args_json) {
|
||||
}
|
||||
|
||||
cJSON_AddBoolToObject(out, "success", 1);
|
||||
cJSON_AddStringToObject(out, "agent_kind0_json", profile_json);
|
||||
|
||||
size_t content_len = strlen("## Agent Kind 0 Profile (source: nostr kind 0)\n\nAgent kind 0 profile content (JSON): ") + strlen(profile_json) + 1U;
|
||||
char* content = (char*)malloc(content_len);
|
||||
@@ -134,7 +131,6 @@ char* execute_nostr_agent_contacts(tools_context_t* ctx, const char* args_json)
|
||||
}
|
||||
|
||||
cJSON_AddBoolToObject(out, "success", 1);
|
||||
cJSON_AddStringToObject(out, "agent_kind3_json", contacts_json);
|
||||
|
||||
size_t content_len = strlen("## Agent Kind 3 Contacts (source: nostr kind 3)\n\nAgent contacts (JSON): ") + strlen(contacts_json) + 1U;
|
||||
char* content = (char*)malloc(content_len);
|
||||
@@ -173,7 +169,6 @@ char* execute_nostr_agent_relays(tools_context_t* ctx, const char* args_json) {
|
||||
}
|
||||
|
||||
cJSON_AddBoolToObject(out, "success", 1);
|
||||
cJSON_AddStringToObject(out, "agent_kind10002_json", relays_json);
|
||||
|
||||
size_t content_len = strlen("## Agent Kind 10002 Relays (source: nostr kind 10002)\n\nAgent relay list (JSON): ") + strlen(relays_json) + 1U;
|
||||
char* content = (char*)malloc(content_len);
|
||||
@@ -212,7 +207,6 @@ char* execute_nostr_agent_notes(tools_context_t* ctx, const char* args_json) {
|
||||
}
|
||||
|
||||
cJSON_AddBoolToObject(out, "success", 1);
|
||||
cJSON_AddStringToObject(out, "agent_notes_content", notes_text);
|
||||
|
||||
size_t content_len = strlen("## Agent Recent Kind 1 Notes\n\n") + strlen(notes_text) + 1U;
|
||||
char* content = (char*)malloc(content_len);
|
||||
@@ -267,7 +261,6 @@ char* execute_adopted_skills(tools_context_t* ctx, const char* args_json) {
|
||||
}
|
||||
cJSON_AddBoolToObject(out, "success", 1);
|
||||
cJSON_AddStringToObject(out, "content", (events_json && events_json[0]) ? events_json : "[]");
|
||||
cJSON_AddStringToObject(out, "adoption_events_json", (events_json && events_json[0]) ? events_json : "[]");
|
||||
|
||||
char* json = cJSON_PrintUnformatted(out);
|
||||
cJSON_Delete(out);
|
||||
@@ -302,7 +295,6 @@ char* execute_trigger_event(tools_context_t* ctx, const char* args_json) {
|
||||
if (!out) return NULL;
|
||||
cJSON_AddBoolToObject(out, "success", 1);
|
||||
cJSON_AddStringToObject(out, "content", trigger_json);
|
||||
cJSON_AddStringToObject(out, "trigger_event_json", trigger_json);
|
||||
char* json = cJSON_PrintUnformatted(out);
|
||||
cJSON_Delete(out);
|
||||
return json;
|
||||
|
||||
@@ -143,6 +143,9 @@ char* execute_config_store(tools_context_t* ctx, const char* args_json) {
|
||||
return json_error_local("config_store requires content");
|
||||
}
|
||||
|
||||
char d_tag_buf[128];
|
||||
snprintf(d_tag_buf, sizeof(d_tag_buf), "%s", d_tag_j->valuestring);
|
||||
|
||||
char* content_text = NULL;
|
||||
if (cJSON_IsString(content_j) && content_j->valuestring) {
|
||||
content_text = strdup(content_j->valuestring);
|
||||
@@ -180,7 +183,7 @@ char* execute_config_store(tools_context_t* ctx, const char* args_json) {
|
||||
}
|
||||
|
||||
cJSON_AddItemToArray(d_tag, cJSON_CreateString("d"));
|
||||
cJSON_AddItemToArray(d_tag, cJSON_CreateString(d_tag_j->valuestring));
|
||||
cJSON_AddItemToArray(d_tag, cJSON_CreateString(d_tag_buf));
|
||||
cJSON_AddItemToArray(tags, d_tag);
|
||||
|
||||
cJSON_AddItemToArray(app_tag, cJSON_CreateString("app"));
|
||||
@@ -208,7 +211,7 @@ char* execute_config_store(tools_context_t* ctx, const char* args_json) {
|
||||
}
|
||||
|
||||
cJSON_AddBoolToObject(out, "success", 1);
|
||||
cJSON_AddStringToObject(out, "d_tag", d_tag_j->valuestring);
|
||||
cJSON_AddStringToObject(out, "d_tag", d_tag_buf);
|
||||
cJSON_AddNumberToObject(out, "kind", CONFIG_KIND);
|
||||
cJSON_AddNumberToObject(out, "content_length", (double)strlen(content_text));
|
||||
cJSON_AddStringToObject(out, "event_id", publish_result.event_id);
|
||||
|
||||
+83
-91
@@ -30,113 +30,72 @@ static cJSON* parse_args_local(const char* args_json) {
|
||||
return args;
|
||||
}
|
||||
|
||||
static int json_object_set_string(cJSON* obj, const char* key, const char* value) {
|
||||
if (!obj || !key || !value) return -1;
|
||||
cJSON_DeleteItemFromObjectCaseSensitive(obj, key);
|
||||
cJSON* v = cJSON_CreateString(value);
|
||||
if (!v) return -1;
|
||||
cJSON_AddItemToObject(obj, key, v);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static char* read_entire_file_local(const char* file_path, size_t* out_bytes) {
|
||||
if (!file_path) return NULL;
|
||||
|
||||
FILE* fp = fopen(file_path, "rb");
|
||||
if (!fp) return NULL;
|
||||
|
||||
if (fseek(fp, 0, SEEK_END) != 0) {
|
||||
fclose(fp);
|
||||
return NULL;
|
||||
static int persist_llm_config_nostr(tools_context_t* ctx, const llm_config_t* cfg, char** out_error) {
|
||||
if (!ctx || !ctx->cfg || !cfg) {
|
||||
if (out_error) *out_error = strdup("invalid persist context");
|
||||
return -1;
|
||||
}
|
||||
|
||||
long len = ftell(fp);
|
||||
if (len < 0) {
|
||||
fclose(fp);
|
||||
return NULL;
|
||||
if (out_error) *out_error = NULL;
|
||||
|
||||
cJSON* args = cJSON_CreateObject();
|
||||
cJSON* content = cJSON_CreateObject();
|
||||
if (!args || !content) {
|
||||
cJSON_Delete(args);
|
||||
cJSON_Delete(content);
|
||||
if (out_error) *out_error = strdup("allocation failure while building llm_config payload");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (fseek(fp, 0, SEEK_SET) != 0) {
|
||||
fclose(fp);
|
||||
return NULL;
|
||||
cJSON_AddStringToObject(args, "d_tag", "llm_config");
|
||||
cJSON_AddStringToObject(content, "provider", cfg->provider);
|
||||
cJSON_AddStringToObject(content, "api_key", cfg->api_key);
|
||||
cJSON_AddStringToObject(content, "model", cfg->model);
|
||||
cJSON_AddStringToObject(content, "base_url", cfg->base_url);
|
||||
cJSON_AddNumberToObject(content, "max_tokens", cfg->max_tokens);
|
||||
cJSON_AddNumberToObject(content, "temperature", cfg->temperature);
|
||||
cJSON_AddItemToObject(args, "content", content);
|
||||
|
||||
char* store_args_json = cJSON_PrintUnformatted(args);
|
||||
cJSON_Delete(args);
|
||||
if (!store_args_json) {
|
||||
if (out_error) *out_error = strdup("failed to serialize llm_config payload");
|
||||
return -1;
|
||||
}
|
||||
|
||||
char* buf = (char*)malloc((size_t)len + 1U);
|
||||
if (!buf) {
|
||||
fclose(fp);
|
||||
return NULL;
|
||||
char* store_result = execute_config_store(ctx, store_args_json);
|
||||
free(store_args_json);
|
||||
if (!store_result) {
|
||||
if (out_error) *out_error = strdup("config_store returned no response");
|
||||
return -1;
|
||||
}
|
||||
|
||||
size_t n = fread(buf, 1, (size_t)len, fp);
|
||||
fclose(fp);
|
||||
if (n != (size_t)len) {
|
||||
free(buf);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
buf[len] = '\0';
|
||||
if (out_bytes) *out_bytes = (size_t)len;
|
||||
return buf;
|
||||
}
|
||||
|
||||
static int persist_llm_config(tools_context_t* ctx, const llm_config_t* cfg) {
|
||||
if (!ctx || !ctx->cfg || !cfg) return -1;
|
||||
if (ctx->cfg->config_path[0] == '\0') return -1;
|
||||
|
||||
size_t src_len = 0;
|
||||
char* raw = read_entire_file_local(ctx->cfg->config_path, &src_len);
|
||||
if (!raw) return -1;
|
||||
|
||||
char* src = jsonc_strip_comments(raw, src_len);
|
||||
free(raw);
|
||||
if (!src) return -1;
|
||||
|
||||
cJSON* root = cJSON_ParseWithLength(src, strlen(src));
|
||||
free(src);
|
||||
cJSON* root = cJSON_Parse(store_result);
|
||||
if (!root || !cJSON_IsObject(root)) {
|
||||
cJSON_Delete(root);
|
||||
if (out_error) *out_error = strdup("config_store returned invalid JSON");
|
||||
free(store_result);
|
||||
return -1;
|
||||
}
|
||||
|
||||
cJSON* llm = cJSON_GetObjectItemCaseSensitive(root, "llm");
|
||||
if (!llm || !cJSON_IsObject(llm)) {
|
||||
cJSON_DeleteItemFromObjectCaseSensitive(root, "llm");
|
||||
llm = cJSON_CreateObject();
|
||||
if (!llm) {
|
||||
cJSON_Delete(root);
|
||||
return -1;
|
||||
cJSON* success = cJSON_GetObjectItemCaseSensitive(root, "success");
|
||||
if (!success || !cJSON_IsBool(success) || !cJSON_IsTrue(success)) {
|
||||
cJSON* err = cJSON_GetObjectItemCaseSensitive(root, "error");
|
||||
if (out_error) {
|
||||
if (err && cJSON_IsString(err) && err->valuestring) {
|
||||
*out_error = strdup(err->valuestring);
|
||||
} else {
|
||||
*out_error = strdup("config_store failed");
|
||||
}
|
||||
}
|
||||
cJSON_AddItemToObject(root, "llm", llm);
|
||||
}
|
||||
|
||||
if (json_object_set_string(llm, "provider", cfg->provider) != 0 ||
|
||||
json_object_set_string(llm, "api_key", cfg->api_key) != 0 ||
|
||||
json_object_set_string(llm, "model", cfg->model) != 0 ||
|
||||
json_object_set_string(llm, "base_url", cfg->base_url) != 0) {
|
||||
cJSON_Delete(root);
|
||||
free(store_result);
|
||||
return -1;
|
||||
}
|
||||
|
||||
cJSON_DeleteItemFromObjectCaseSensitive(llm, "max_tokens");
|
||||
cJSON_AddNumberToObject(llm, "max_tokens", cfg->max_tokens);
|
||||
cJSON_DeleteItemFromObjectCaseSensitive(llm, "temperature");
|
||||
cJSON_AddNumberToObject(llm, "temperature", cfg->temperature);
|
||||
|
||||
char* out = cJSON_Print(root);
|
||||
cJSON_Delete(root);
|
||||
if (!out) return -1;
|
||||
|
||||
FILE* fp = fopen(ctx->cfg->config_path, "wb");
|
||||
if (!fp) {
|
||||
free(out);
|
||||
return -1;
|
||||
}
|
||||
|
||||
size_t out_len = strlen(out);
|
||||
size_t n = fwrite(out, 1, out_len, fp);
|
||||
fclose(fp);
|
||||
free(out);
|
||||
return n == out_len ? 0 : -1;
|
||||
free(store_result);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int assign_string_field(cJSON* item, char* dst, size_t dst_size, int* changed) {
|
||||
@@ -149,6 +108,25 @@ static int assign_string_field(cJSON* item, char* dst, size_t dst_size, int* cha
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void mask_api_key_local(const char* api_key, char* out, size_t out_size) {
|
||||
if (!out || out_size == 0) return;
|
||||
out[0] = '\0';
|
||||
if (!api_key || api_key[0] == '\0') {
|
||||
return;
|
||||
}
|
||||
|
||||
size_t api_len = strlen(api_key);
|
||||
if (api_len >= 8U) {
|
||||
snprintf(out,
|
||||
out_size,
|
||||
"%.4s...%s",
|
||||
api_key,
|
||||
api_key + api_len - 4U);
|
||||
} else {
|
||||
snprintf(out, out_size, "%s", "(set)");
|
||||
}
|
||||
}
|
||||
|
||||
char* execute_model_get(const char* args_json) {
|
||||
cJSON* args = parse_args_local(args_json);
|
||||
if (!args) return json_error_local("invalid arguments JSON");
|
||||
@@ -161,10 +139,15 @@ char* execute_model_get(const char* args_json) {
|
||||
|
||||
cJSON* out = cJSON_CreateObject();
|
||||
if (!out) return NULL;
|
||||
|
||||
char api_key_masked[64] = {0};
|
||||
mask_api_key_local(cfg.api_key, api_key_masked, sizeof(api_key_masked));
|
||||
|
||||
cJSON_AddBoolToObject(out, "success", 1);
|
||||
cJSON_AddStringToObject(out, "provider", cfg.provider);
|
||||
cJSON_AddStringToObject(out, "model", cfg.model);
|
||||
cJSON_AddStringToObject(out, "base_url", cfg.base_url);
|
||||
cJSON_AddStringToObject(out, "api_key", api_key_masked);
|
||||
cJSON_AddNumberToObject(out, "max_tokens", cfg.max_tokens);
|
||||
cJSON_AddNumberToObject(out, "temperature", cfg.temperature);
|
||||
|
||||
@@ -231,21 +214,30 @@ char* execute_model_set(tools_context_t* ctx, const char* args_json) {
|
||||
}
|
||||
|
||||
ctx->cfg->llm = cfg;
|
||||
if (persist_llm_config(ctx, &cfg) != 0) {
|
||||
return json_error_local("failed to persist llm config to config file");
|
||||
}
|
||||
|
||||
char* persist_error = NULL;
|
||||
int persisted = (persist_llm_config_nostr(ctx, &cfg, &persist_error) == 0) ? 1 : 0;
|
||||
|
||||
cJSON* out = cJSON_CreateObject();
|
||||
if (!out) return NULL;
|
||||
char api_key_masked[64] = {0};
|
||||
mask_api_key_local(cfg.api_key, api_key_masked, sizeof(api_key_masked));
|
||||
|
||||
cJSON_AddBoolToObject(out, "success", 1);
|
||||
cJSON_AddStringToObject(out, "provider", cfg.provider);
|
||||
cJSON_AddStringToObject(out, "model", cfg.model);
|
||||
cJSON_AddStringToObject(out, "base_url", cfg.base_url);
|
||||
cJSON_AddStringToObject(out, "api_key", api_key_masked);
|
||||
cJSON_AddNumberToObject(out, "max_tokens", cfg.max_tokens);
|
||||
cJSON_AddNumberToObject(out, "temperature", cfg.temperature);
|
||||
cJSON_AddBoolToObject(out, "persisted", persisted ? 1 : 0);
|
||||
if (!persisted && persist_error && persist_error[0] != '\0') {
|
||||
cJSON_AddStringToObject(out, "persist_warning", persist_error);
|
||||
}
|
||||
|
||||
char* json = cJSON_PrintUnformatted(out);
|
||||
cJSON_Delete(out);
|
||||
free(persist_error);
|
||||
return json;
|
||||
}
|
||||
|
||||
|
||||
+150
-86
@@ -2,14 +2,18 @@
|
||||
|
||||
#include "tools_internal.h"
|
||||
|
||||
#include <limits.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "cjson/cJSON.h"
|
||||
#include "../nostr_handler.h"
|
||||
#include "../../nostr_core_lib/nostr_core/nostr_core.h"
|
||||
|
||||
#define TASKS_KIND 30078
|
||||
#define TASKS_D_TAG "tasks"
|
||||
#define TASKS_APP_TAG "didactyl"
|
||||
|
||||
static char* json_error_local(const char* msg) {
|
||||
cJSON* root = cJSON_CreateObject();
|
||||
@@ -31,33 +35,6 @@ static cJSON* parse_args_local(const char* args_json) {
|
||||
return args;
|
||||
}
|
||||
|
||||
static int is_safe_relative_path_local(const char* path) {
|
||||
if (!path || path[0] == '\0') return 0;
|
||||
if (path[0] == '/') return 0;
|
||||
if (strstr(path, "..") != NULL) return 0;
|
||||
if (strchr(path, '\\') != NULL) return 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int build_tool_path_local(tools_context_t* ctx, const char* rel_path, char* out, size_t out_size) {
|
||||
if (!ctx || !ctx->cfg || !rel_path || !out || out_size == 0) return -1;
|
||||
if (!is_safe_relative_path_local(rel_path)) return -1;
|
||||
|
||||
const char* cwd = ctx->cfg->tools.shell.working_directory[0] != '\0'
|
||||
? ctx->cfg->tools.shell.working_directory
|
||||
: ".";
|
||||
|
||||
int n = 0;
|
||||
if (strcmp(cwd, ".") == 0) {
|
||||
n = snprintf(out, out_size, "%s", rel_path);
|
||||
} else {
|
||||
n = snprintf(out, out_size, "%s/%s", cwd, rel_path);
|
||||
}
|
||||
|
||||
if (n < 0 || (size_t)n >= out_size) return -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static cJSON* tasks_create_empty_root_local(void) {
|
||||
cJSON* root = cJSON_CreateObject();
|
||||
cJSON* tasks = cJSON_CreateArray();
|
||||
@@ -79,47 +56,118 @@ static const char* normalize_task_status_local(const char* status) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static cJSON* tasks_load_root_local(const char* path) {
|
||||
if (!path) return NULL;
|
||||
static int nip44_encrypt_self_local(tools_context_t* ctx, const char* plaintext, char** out_ciphertext) {
|
||||
if (!ctx || !ctx->cfg || !out_ciphertext) return -1;
|
||||
|
||||
FILE* fp = fopen(path, "rb");
|
||||
if (!fp) {
|
||||
if (access(path, F_OK) == 0) {
|
||||
return NULL;
|
||||
}
|
||||
const char* plain = plaintext ? plaintext : "";
|
||||
size_t out_cap = (strlen(plain) * 4U) + 1024U;
|
||||
char* ciphertext = (char*)malloc(out_cap);
|
||||
if (!ciphertext) return -1;
|
||||
|
||||
int rc = nostr_nip44_encrypt(ctx->cfg->keys.private_key,
|
||||
ctx->cfg->keys.public_key,
|
||||
plain,
|
||||
ciphertext,
|
||||
out_cap);
|
||||
if (rc != NOSTR_SUCCESS) {
|
||||
free(ciphertext);
|
||||
return -1;
|
||||
}
|
||||
|
||||
*out_ciphertext = ciphertext;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int nip44_decrypt_self_local(tools_context_t* ctx, const char* ciphertext, char** out_plaintext) {
|
||||
if (!ctx || !ctx->cfg || !ciphertext || !out_plaintext) return -1;
|
||||
|
||||
size_t out_cap = strlen(ciphertext) + 1024U;
|
||||
char* plaintext = (char*)malloc(out_cap);
|
||||
if (!plaintext) return -1;
|
||||
|
||||
int rc = nostr_nip44_decrypt(ctx->cfg->keys.private_key,
|
||||
ctx->cfg->keys.public_key,
|
||||
ciphertext,
|
||||
plaintext,
|
||||
out_cap);
|
||||
if (rc != NOSTR_SUCCESS) {
|
||||
free(plaintext);
|
||||
return -1;
|
||||
}
|
||||
|
||||
*out_plaintext = plaintext;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int query_tasks_ciphertext_local(tools_context_t* ctx, char** out_ciphertext) {
|
||||
if (!ctx || !ctx->cfg || !out_ciphertext) return -1;
|
||||
|
||||
cJSON* filter = cJSON_CreateObject();
|
||||
cJSON* kinds = cJSON_CreateArray();
|
||||
cJSON* authors = cJSON_CreateArray();
|
||||
cJSON* d_vals = cJSON_CreateArray();
|
||||
if (!filter || !kinds || !authors || !d_vals) {
|
||||
cJSON_Delete(filter);
|
||||
cJSON_Delete(kinds);
|
||||
cJSON_Delete(authors);
|
||||
cJSON_Delete(d_vals);
|
||||
return -1;
|
||||
}
|
||||
|
||||
cJSON_AddItemToArray(kinds, cJSON_CreateNumber(TASKS_KIND));
|
||||
cJSON_AddItemToObject(filter, "kinds", kinds);
|
||||
|
||||
cJSON_AddItemToArray(authors, cJSON_CreateString(ctx->cfg->keys.public_key_hex));
|
||||
cJSON_AddItemToObject(filter, "authors", authors);
|
||||
|
||||
cJSON_AddItemToArray(d_vals, cJSON_CreateString(TASKS_D_TAG));
|
||||
cJSON_AddItemToObject(filter, "#d", d_vals);
|
||||
|
||||
cJSON_AddNumberToObject(filter, "limit", 1);
|
||||
|
||||
char* events_json = nostr_handler_query_json(filter, 4000);
|
||||
cJSON_Delete(filter);
|
||||
if (!events_json) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
cJSON* arr = cJSON_Parse(events_json);
|
||||
free(events_json);
|
||||
if (!arr || !cJSON_IsArray(arr) || cJSON_GetArraySize(arr) <= 0) {
|
||||
cJSON_Delete(arr);
|
||||
*out_ciphertext = strdup("");
|
||||
return (*out_ciphertext != NULL) ? 0 : -1;
|
||||
}
|
||||
|
||||
cJSON* ev = cJSON_GetArrayItem(arr, 0);
|
||||
cJSON* content = ev ? cJSON_GetObjectItemCaseSensitive(ev, "content") : NULL;
|
||||
const char* cipher = (content && cJSON_IsString(content) && content->valuestring) ? content->valuestring : "";
|
||||
|
||||
*out_ciphertext = strdup(cipher);
|
||||
cJSON_Delete(arr);
|
||||
return (*out_ciphertext != NULL) ? 0 : -1;
|
||||
}
|
||||
|
||||
static cJSON* tasks_load_root_nostr_local(tools_context_t* ctx) {
|
||||
char* ciphertext = NULL;
|
||||
if (query_tasks_ciphertext_local(ctx, &ciphertext) != 0) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (!ciphertext || ciphertext[0] == '\0') {
|
||||
free(ciphertext);
|
||||
return tasks_create_empty_root_local();
|
||||
}
|
||||
|
||||
if (fseek(fp, 0, SEEK_END) != 0) {
|
||||
fclose(fp);
|
||||
return NULL;
|
||||
}
|
||||
long len = ftell(fp);
|
||||
if (len < 0) {
|
||||
fclose(fp);
|
||||
return NULL;
|
||||
}
|
||||
if (fseek(fp, 0, SEEK_SET) != 0) {
|
||||
fclose(fp);
|
||||
char* plaintext = NULL;
|
||||
if (nip44_decrypt_self_local(ctx, ciphertext, &plaintext) != 0) {
|
||||
free(ciphertext);
|
||||
return NULL;
|
||||
}
|
||||
free(ciphertext);
|
||||
|
||||
char* buf = (char*)malloc((size_t)len + 1U);
|
||||
if (!buf) {
|
||||
fclose(fp);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
size_t n = fread(buf, 1, (size_t)len, fp);
|
||||
fclose(fp);
|
||||
if (n != (size_t)len) {
|
||||
free(buf);
|
||||
return NULL;
|
||||
}
|
||||
buf[len] = '\0';
|
||||
|
||||
cJSON* root = cJSON_Parse(buf);
|
||||
free(buf);
|
||||
cJSON* root = cJSON_Parse(plaintext);
|
||||
free(plaintext);
|
||||
if (!root || !cJSON_IsObject(root)) {
|
||||
cJSON_Delete(root);
|
||||
return NULL;
|
||||
@@ -140,22 +188,45 @@ static cJSON* tasks_load_root_local(const char* path) {
|
||||
return root;
|
||||
}
|
||||
|
||||
static int tasks_save_root_local(const char* path, cJSON* root) {
|
||||
if (!path || !root) return -1;
|
||||
char* raw = cJSON_PrintUnformatted(root);
|
||||
if (!raw) return -1;
|
||||
static int tasks_save_root_nostr_local(tools_context_t* ctx, cJSON* root) {
|
||||
if (!ctx || !ctx->cfg || !root) return -1;
|
||||
|
||||
FILE* fp = fopen(path, "wb");
|
||||
if (!fp) {
|
||||
free(raw);
|
||||
char* plaintext = cJSON_PrintUnformatted(root);
|
||||
if (!plaintext) return -1;
|
||||
|
||||
char* ciphertext = NULL;
|
||||
if (nip44_encrypt_self_local(ctx, plaintext, &ciphertext) != 0) {
|
||||
free(plaintext);
|
||||
return -1;
|
||||
}
|
||||
free(plaintext);
|
||||
|
||||
cJSON* tags = cJSON_CreateArray();
|
||||
cJSON* d_tag = cJSON_CreateArray();
|
||||
cJSON* app_tag = cJSON_CreateArray();
|
||||
if (!tags || !d_tag || !app_tag) {
|
||||
cJSON_Delete(tags);
|
||||
cJSON_Delete(d_tag);
|
||||
cJSON_Delete(app_tag);
|
||||
free(ciphertext);
|
||||
return -1;
|
||||
}
|
||||
|
||||
size_t len = strlen(raw);
|
||||
size_t n = fwrite(raw, 1, len, fp);
|
||||
fclose(fp);
|
||||
free(raw);
|
||||
return (n == len) ? 0 : -1;
|
||||
cJSON_AddItemToArray(d_tag, cJSON_CreateString("d"));
|
||||
cJSON_AddItemToArray(d_tag, cJSON_CreateString(TASKS_D_TAG));
|
||||
cJSON_AddItemToArray(tags, d_tag);
|
||||
|
||||
cJSON_AddItemToArray(app_tag, cJSON_CreateString("app"));
|
||||
cJSON_AddItemToArray(app_tag, cJSON_CreateString(TASKS_APP_TAG));
|
||||
cJSON_AddItemToArray(tags, app_tag);
|
||||
|
||||
nostr_publish_result_t publish_result;
|
||||
memset(&publish_result, 0, sizeof(publish_result));
|
||||
int rc = nostr_handler_publish_kind_event(TASKS_KIND, ciphertext, tags, &publish_result);
|
||||
cJSON_Delete(tags);
|
||||
free(ciphertext);
|
||||
nostr_handler_publish_result_free(&publish_result);
|
||||
return rc == 0 ? 0 : -1;
|
||||
}
|
||||
|
||||
static cJSON* task_find_by_id_local(cJSON* tasks, int id, int* out_index) {
|
||||
@@ -184,16 +255,10 @@ char* execute_task_manage(tools_context_t* ctx, const char* args_json) {
|
||||
return json_error_local("task_manage requires string action");
|
||||
}
|
||||
|
||||
char tasks_path[PATH_MAX];
|
||||
if (build_tool_path_local(ctx, "tasks.json", tasks_path, sizeof(tasks_path)) != 0) {
|
||||
cJSON_Delete(args);
|
||||
return json_error_local("failed to resolve tasks file path");
|
||||
}
|
||||
|
||||
cJSON* root = tasks_load_root_local(tasks_path);
|
||||
cJSON* root = tasks_load_root_nostr_local(ctx);
|
||||
if (!root) {
|
||||
cJSON_Delete(args);
|
||||
return json_error_local("failed to load tasks file");
|
||||
return json_error_local("failed to load tasks memory");
|
||||
}
|
||||
|
||||
cJSON* tasks = cJSON_GetObjectItemCaseSensitive(root, "tasks");
|
||||
@@ -405,10 +470,10 @@ char* execute_task_manage(tools_context_t* ctx, const char* args_json) {
|
||||
if (mutated) {
|
||||
cJSON_DeleteItemFromObjectCaseSensitive(root, "next_id");
|
||||
cJSON_AddNumberToObject(root, "next_id", next_id);
|
||||
if (tasks_save_root_local(tasks_path, root) != 0) {
|
||||
if (tasks_save_root_nostr_local(ctx, root) != 0) {
|
||||
cJSON_Delete(args);
|
||||
cJSON_Delete(root);
|
||||
return json_error_local("failed to save tasks file");
|
||||
return json_error_local("failed to persist tasks memory");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -421,7 +486,6 @@ char* execute_task_manage(tools_context_t* ctx, const char* args_json) {
|
||||
|
||||
cJSON_AddBoolToObject(out, "success", 1);
|
||||
cJSON_AddStringToObject(out, "action", action_s);
|
||||
cJSON_AddStringToObject(out, "path", tasks_path);
|
||||
cJSON_AddBoolToObject(out, "mutated", mutated ? 1 : 0);
|
||||
cJSON_AddNumberToObject(out, "count", cJSON_GetArraySize(tasks));
|
||||
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
#ifndef DIDACTYL_TOOLS_H
|
||||
#define DIDACTYL_TOOLS_H
|
||||
|
||||
#include "config.h"
|
||||
#include "../config.h"
|
||||
|
||||
struct trigger_manager;
|
||||
|
||||
|
||||
@@ -687,6 +687,9 @@ char* tools_build_openai_schema_json_legacy(const tools_context_t* ctx) {
|
||||
|
||||
cJSON* p_skill_create_llm = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(p_skill_create_llm, "type", "string");
|
||||
cJSON_AddStringToObject(p_skill_create_llm,
|
||||
"description",
|
||||
"Model to use when this trigger fires (e.g. 'gpt-4o-mini'); overrides the agent default model for this triggered skill execution only.");
|
||||
cJSON_AddItemToObject(t22_props, "llm", p_skill_create_llm);
|
||||
|
||||
cJSON* p_skill_create_tools = cJSON_CreateObject();
|
||||
@@ -840,6 +843,9 @@ char* tools_build_openai_schema_json_legacy(const tools_context_t* ctx) {
|
||||
|
||||
cJSON* p_skill_edit_llm = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(p_skill_edit_llm, "type", "string");
|
||||
cJSON_AddStringToObject(p_skill_edit_llm,
|
||||
"description",
|
||||
"Model to use when this trigger fires (e.g. 'gpt-4o-mini'); overrides the agent default model for this triggered skill execution only.");
|
||||
cJSON_AddItemToObject(t25b_props, "llm", p_skill_edit_llm);
|
||||
|
||||
cJSON* p_skill_edit_tools = cJSON_CreateObject();
|
||||
@@ -976,7 +982,7 @@ char* tools_build_openai_schema_json_legacy(const tools_context_t* ctx) {
|
||||
|
||||
cJSON_AddStringToObject(t31, "type", "function");
|
||||
cJSON_AddStringToObject(t31_fn, "name", "model_set");
|
||||
cJSON_AddStringToObject(t31_fn, "description", "Update active LLM configuration and persist it to config.jsonc");
|
||||
cJSON_AddStringToObject(t31_fn, "description", "Update active LLM configuration and persist it to Nostr kind 30078 (d=llm_config)");
|
||||
cJSON_AddStringToObject(t31_params, "type", "object");
|
||||
cJSON_AddItemToObject(t31_params, "properties", t31_props);
|
||||
|
||||
@@ -1276,7 +1282,7 @@ char* tools_build_openai_schema_json_legacy(const tools_context_t* ctx) {
|
||||
|
||||
cJSON_AddStringToObject(t42, "type", "function");
|
||||
cJSON_AddStringToObject(t42_fn, "name", "task_list");
|
||||
cJSON_AddStringToObject(t42_fn, "description", "Build current task list context block from tasks.json");
|
||||
cJSON_AddStringToObject(t42_fn, "description", "Build current task list context block from agent task memory on Nostr");
|
||||
cJSON_AddStringToObject(t42_params, "type", "object");
|
||||
cJSON_AddItemToObject(t42_params, "properties", t42_props);
|
||||
cJSON_AddItemToObject(t42_fn, "parameters", t42_params);
|
||||
@@ -1305,7 +1311,7 @@ char* tools_build_openai_schema_json_legacy(const tools_context_t* ctx) {
|
||||
|
||||
cJSON_AddStringToObject(t45, "type", "function");
|
||||
cJSON_AddStringToObject(t45_fn, "name", "task_manage");
|
||||
cJSON_AddStringToObject(t45_fn, "description", "Manage agent short-term task memory stored in tasks.json (list/add/update/remove/clear/replace)");
|
||||
cJSON_AddStringToObject(t45_fn, "description", "Manage agent short-term task memory stored on Nostr kind 30078 (d=tasks): list/add/update/remove/clear/replace");
|
||||
cJSON_AddStringToObject(t45_params, "type", "object");
|
||||
cJSON_AddItemToObject(t45_params, "properties", t45_props);
|
||||
cJSON_AddItemToObject(t45_params, "required", t45_required);
|
||||
|
||||
Reference in New Issue
Block a user