Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
14a4f6f079 | ||
|
|
5efe932a5f |
@@ -121,7 +121,7 @@ RUN NOSTR_LIB=$(ls /build/nostr_core_lib/libnostr_core_*.a 2>/dev/null | head -1
|
||||
src/tools/tool_nostr_query.c src/tools/tool_nostr_my_events.c src/tools/tool_nostr_identity.c src/tools/tool_nostr_social.c \
|
||||
src/tools/tool_nostr_relay.c src/tools/tool_nostr_dm.c src/tools/tool_admin.c \
|
||||
src/tools/tool_task.c src/tools/tool_nostr_list.c src/tools/tool_nostr_block.c src/tools/tool_local.c \
|
||||
src/tools/tool_skill.c src/tools/tool_nostr_post.c src/tools/tool_memory.c src/tools/tool_config.c src/tools/tool_cashu_wallet.c src/trigger_manager.c \
|
||||
src/tools/tool_skill.c src/tools/tool_nostr_post.c src/tools/tool_memory.c src/tools/tool_config.c src/tools/tool_cashu_wallet.c src/tools/tool_blossom.c src/trigger_manager.c \
|
||||
src/cashu_wallet.c src/nostr_block_list.c src/prompt_template.c src/http_api.c src/setup_wizard.c src/mongoose.c src/debug.c \
|
||||
-o /build/didactyl_static \
|
||||
$NOSTR_LIB \
|
||||
|
||||
@@ -34,6 +34,7 @@ SRCS = \
|
||||
$(SRC_DIR)/tools/tool_memory.c \
|
||||
$(SRC_DIR)/tools/tool_config.c \
|
||||
$(SRC_DIR)/tools/tool_cashu_wallet.c \
|
||||
$(SRC_DIR)/tools/tool_blossom.c \
|
||||
$(SRC_DIR)/trigger_manager.c \
|
||||
$(SRC_DIR)/prompt_template.c \
|
||||
$(SRC_DIR)/http_api.c \
|
||||
|
||||
@@ -55,11 +55,11 @@ Skills compose by adoption-list order (`10123`) and trigger tags carry runtime e
|
||||
|
||||
Didactyl will support local inference, which is very privacy preserving. Remote inference does however have it's advantages, and in those cases Didactyl supports using Bitcoin Lightning and eCash inference providers.
|
||||
|
||||
## Current Status — v0.1.19
|
||||
## Current Status — v0.1.21
|
||||
|
||||
**Active build — this project is barely working. Experiment at your own risk.**
|
||||
|
||||
> Last release update: v0.1.19 — Prevent ignored spent token events from reloading into wallet balance after restart
|
||||
> Last release update: v0.1.21 — Fix segfault in nostr_http_request by copying CURLINFO_CONTENT_TYPE before curl cleanup and update debug deploy script to deploy *_debug binary for symbolized coredumps
|
||||
|
||||
- Connects to configured relays with auto-reconnect and relay state transition logging
|
||||
- Publishes configured startup events per relay as each relay becomes connected
|
||||
|
||||
@@ -62,6 +62,8 @@
|
||||
"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,
|
||||
"blossom_max_upload_bytes": 16777216,
|
||||
"blossom_max_download_bytes": 16777216,
|
||||
"shell": {
|
||||
"enabled": true,
|
||||
"timeout_seconds": 30,
|
||||
|
||||
Executable
+109
@@ -0,0 +1,109 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Debug variant of deploy_lt.sh
|
||||
# Builds with debug symbols, deploys, enables core dumps, and restarts services.
|
||||
# After a crash, run on the server:
|
||||
# coredumpctl list
|
||||
# coredumpctl gdb simon.service
|
||||
# (gdb) bt full
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
SOURCE_BINARY_DEBUG="$SCRIPT_DIR/didactyl_static_x86_64_debug"
|
||||
SOURCE_BINARY_FALLBACK="$SCRIPT_DIR/didactyl_static_x86_64"
|
||||
SOURCE_BINARY=""
|
||||
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 ! command -v ssh >/dev/null 2>&1; then
|
||||
echo "ERROR: ssh is not installed or not in PATH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "=========================================="
|
||||
echo " DEBUG BUILD + DEPLOY"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
|
||||
echo "Building DEBUG static binary (symbols preserved)..."
|
||||
"$SCRIPT_DIR/build_static.sh" --debug
|
||||
|
||||
if [ -f "$SOURCE_BINARY_DEBUG" ]; then
|
||||
SOURCE_BINARY="$SOURCE_BINARY_DEBUG"
|
||||
elif [ -f "$SOURCE_BINARY_FALLBACK" ]; then
|
||||
SOURCE_BINARY="$SOURCE_BINARY_FALLBACK"
|
||||
else
|
||||
echo "ERROR: Build completed but no binary found (expected $SOURCE_BINARY_DEBUG or $SOURCE_BINARY_FALLBACK)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Binary info:"
|
||||
file "$SOURCE_BINARY"
|
||||
ls -lh "$SOURCE_BINARY"
|
||||
echo ""
|
||||
|
||||
REMOTE_STAGE="/tmp/didactyl_static_x86_64"
|
||||
|
||||
echo "Uploading debug binary to $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 ""
|
||||
echo "Enabling core dumps on remote..."
|
||||
ssh "$REMOTE_HOST" "
|
||||
# Ensure systemd stores core dumps via coredumpctl
|
||||
sudo mkdir -p /etc/systemd/coredump.conf.d
|
||||
echo '[Coredump]
|
||||
Storage=external
|
||||
ProcessSizeMax=512M
|
||||
ExternalSizeMax=512M' | sudo tee /etc/systemd/coredump.conf.d/debug.conf > /dev/null
|
||||
sudo systemctl daemon-reload
|
||||
|
||||
# Remove any ulimit restrictions for the services
|
||||
for svc in ${REMOTE_SERVICES[*]}; do
|
||||
OVERRIDE_DIR=\"/etc/systemd/system/\${svc}.d\"
|
||||
sudo mkdir -p \"\$OVERRIDE_DIR\"
|
||||
echo '[Service]
|
||||
LimitCORE=infinity' | sudo tee \"\$OVERRIDE_DIR/coredump.conf\" > /dev/null
|
||||
done
|
||||
sudo systemctl daemon-reload
|
||||
"
|
||||
|
||||
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 ""
|
||||
echo "=========================================="
|
||||
echo " DEBUG DEPLOYMENT COMPLETE"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
echo "After reproducing the crash, run on the server:"
|
||||
echo " ssh $REMOTE_HOST"
|
||||
echo " sudo coredumpctl list"
|
||||
echo " sudo coredumpctl gdb simon.service"
|
||||
echo " (gdb) bt full"
|
||||
echo " (gdb) info threads"
|
||||
echo " (gdb) thread apply all bt"
|
||||
echo ""
|
||||
echo "Then paste the backtrace here."
|
||||
@@ -172,6 +172,16 @@ These tools manage the agent's short-term and long-term memory, persisted on Nos
|
||||
| `cashu_wallet_send_token` | Create an outbound ecash token from wallet proofs and return a `cashuA`/`cashuB` token string |
|
||||
| `cashu_wallet_mints_set` | Set wallet mints (NIP-60), public mints (NIP-61), or both |
|
||||
|
||||
### Blossom Tools
|
||||
|
||||
| Tool | Description |
|
||||
|---|---|
|
||||
| `blossom_upload` | Upload a local file to a Blossom server and return blob metadata |
|
||||
| `blossom_download` | Download a Blossom blob to a local file path |
|
||||
| `blossom_head` | Fetch Blossom blob metadata by SHA-256 |
|
||||
| `blossom_delete` | Delete a Blossom blob by SHA-256 using signed auth |
|
||||
| `blossom_list` | List Blossom blobs for a pubkey (defaults to agent pubkey) |
|
||||
|
||||
### Content Publishing Conveniences
|
||||
|
||||
| Tool | Description |
|
||||
|
||||
@@ -0,0 +1,668 @@
|
||||
# Implementation Plan: Blossom Tools for Didactyl
|
||||
|
||||
## Objective
|
||||
|
||||
Add first-class Blossom tooling so the agent can upload, download, inspect, and delete blobs on Blossom-compatible servers. As a prerequisite, consolidate all HTTP client code into a single public API in `nostr_core_lib` so that both projects share one curl implementation.
|
||||
|
||||
---
|
||||
|
||||
## Problem: HTTP Client Duplication
|
||||
|
||||
There are currently **5 separate curl implementations** across the two projects:
|
||||
|
||||
| # | Project | File | Function | Methods | Visibility |
|
||||
|---|---|---|---|---|---|
|
||||
| 1 | nostr_core_lib | `cashu_mint.c:88` | `cashu_http_json_request()` | GET, POST | `static` |
|
||||
| 2 | nostr_core_lib | `nip005.c:103` | `nip05_http_get()` | GET | `static` |
|
||||
| 3 | nostr_core_lib | `nip011.c:344` | inline curl block | GET | `static` |
|
||||
| 4 | Didactyl | `llm.c:88` | `perform_http_request()` | GET, POST | `static` |
|
||||
| 5 | Didactyl | `tool_local.c:214` | inline in `execute_local_http_fetch()` | GET, POST, any | `static` |
|
||||
|
||||
Each has its own write callback, response buffer struct, CA bundle detection, SSL config, and error handling. Adding Blossom without consolidation would create a 6th copy.
|
||||
|
||||
### Decision
|
||||
|
||||
Consolidate all HTTP client code into a **single public API** in `nostr_core_lib`. Migrate all consumers in both projects to use it. Then build Blossom client on top of the same shared HTTP layer.
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Unified HTTP Client in nostr_core_lib
|
||||
|
||||
### New files
|
||||
|
||||
- `nostr_core_lib/nostr_core/nostr_http.h` — public API
|
||||
- `nostr_core_lib/nostr_core/nostr_http.c` — single curl implementation
|
||||
|
||||
### Proposed API
|
||||
|
||||
```c
|
||||
#ifndef NOSTR_HTTP_H
|
||||
#define NOSTR_HTTP_H
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
// HTTP response container
|
||||
typedef struct {
|
||||
char* body; // Response body (malloc'd, caller frees)
|
||||
size_t body_len; // Body length in bytes
|
||||
long status_code; // HTTP status code
|
||||
char* content_type; // Content-Type header value (malloc'd, caller frees)
|
||||
char* headers_raw; // All response headers (malloc'd, caller frees, optional)
|
||||
} nostr_http_response_t;
|
||||
|
||||
// HTTP request options
|
||||
typedef struct {
|
||||
const char* method; // "GET", "POST", "PUT", "DELETE", "HEAD" (default: "GET")
|
||||
const char* url; // Required
|
||||
const char** headers; // NULL-terminated array of "Key: Value" strings (optional)
|
||||
const unsigned char* body; // Request body bytes (optional)
|
||||
size_t body_len; // Body length (0 if no body)
|
||||
int timeout_seconds; // Request timeout (default: 30)
|
||||
size_t max_response_bytes; // Cap response body size (0 = unlimited)
|
||||
int follow_redirects; // 1 = follow, 0 = don't (default: 1)
|
||||
int max_redirects; // Max redirect hops (default: 3)
|
||||
const char* user_agent; // User-Agent header (default: "nostr-core/VERSION")
|
||||
int capture_headers; // 1 = capture response headers in headers_raw
|
||||
} nostr_http_request_t;
|
||||
|
||||
// Set global CA bundle path for all HTTP requests
|
||||
void nostr_http_set_ca_bundle(const char* ca_bundle_path);
|
||||
|
||||
// Auto-detect CA bundle from common system paths
|
||||
const char* nostr_http_detect_ca_bundle(void);
|
||||
|
||||
// Perform an HTTP request
|
||||
// Returns NOSTR_SUCCESS on successful HTTP round-trip (even 4xx/5xx).
|
||||
// Returns NOSTR_ERROR_NETWORK_FAILED on connection/DNS/timeout failure.
|
||||
// Caller must call nostr_http_response_free() on success.
|
||||
int nostr_http_request(const nostr_http_request_t* req, nostr_http_response_t* resp);
|
||||
|
||||
// Free response resources
|
||||
void nostr_http_response_free(nostr_http_response_t* resp);
|
||||
|
||||
// Convenience: simple GET returning body string
|
||||
int nostr_http_get(const char* url, int timeout_seconds, char** body_out, long* status_out);
|
||||
|
||||
// Convenience: JSON POST returning body string
|
||||
int nostr_http_post_json(const char* url, const char* json_body, int timeout_seconds,
|
||||
char** body_out, long* status_out);
|
||||
|
||||
#endif
|
||||
```
|
||||
|
||||
### Key design decisions
|
||||
|
||||
1. **Binary-safe body** — `body` is `unsigned char*` with explicit `body_len`, supporting both JSON text and raw file uploads
|
||||
2. **Method-agnostic** — supports GET, POST, PUT, DELETE, HEAD, PATCH via string
|
||||
3. **Response size cap** — `max_response_bytes` prevents OOM on large downloads
|
||||
4. **Header capture** — optional `capture_headers` for HEAD requests (Blossom needs this)
|
||||
5. **CA bundle** — single global setter replaces 5 separate detection functions
|
||||
6. **Convenience wrappers** — `nostr_http_get()` and `nostr_http_post_json()` cover the common JSON API pattern used by cashu_mint, nip005, nip011
|
||||
|
||||
### Implementation notes
|
||||
|
||||
- Single `static size_t write_callback()` function
|
||||
- Single `static size_t header_callback()` function (for header capture)
|
||||
- CA bundle auto-detection consolidated from the 5 existing implementations
|
||||
- SSL verification always on by default
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Migrate nostr_core_lib Internal Consumers
|
||||
|
||||
### cashu_mint.c
|
||||
|
||||
Replace `cashu_http_json_request()` (static, ~70 lines) with calls to `nostr_http_post_json()` / `nostr_http_get()`.
|
||||
|
||||
**Before:**
|
||||
```c
|
||||
static int cashu_http_json_request(const char* method, const char* url,
|
||||
const char* body, int timeout_seconds,
|
||||
char** response_out, long* status_out) {
|
||||
// 70 lines of curl boilerplate
|
||||
}
|
||||
```
|
||||
|
||||
**After:**
|
||||
```c
|
||||
static int cashu_http_json_request(const char* method, const char* url,
|
||||
const char* body, int timeout_seconds,
|
||||
char** response_out, long* status_out) {
|
||||
if (strcmp(method, "POST") == 0) {
|
||||
return nostr_http_post_json(url, body, timeout_seconds, response_out, status_out);
|
||||
}
|
||||
return nostr_http_get(url, timeout_seconds, response_out, status_out);
|
||||
}
|
||||
```
|
||||
|
||||
The function signature stays the same so all 20+ call sites in cashu_mint.c are unaffected.
|
||||
|
||||
### nip005.c
|
||||
|
||||
Replace `nip05_http_get()` (static, ~50 lines) with `nostr_http_get()`.
|
||||
|
||||
### nip011.c
|
||||
|
||||
Replace inline curl block (~40 lines) with `nostr_http_request()` using custom Accept header.
|
||||
|
||||
### Build changes
|
||||
|
||||
- Add `nostr_http.c` to the nostr_core_lib build
|
||||
- Remove `#include <curl/curl.h>` from cashu_mint.c, nip005.c, nip011.c (only nostr_http.c includes it)
|
||||
- Update `nostr_core.h` to include `nostr_http.h`
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Migrate Didactyl Consumers
|
||||
|
||||
### llm.c
|
||||
|
||||
Replace `perform_http_request()` (static, ~80 lines) with `nostr_http_request()`.
|
||||
|
||||
**Current signature:** `static char* perform_http_request(const char* url, const char* body, int is_post)`
|
||||
|
||||
**Migration:** Build a `nostr_http_request_t` with the Authorization Bearer header, call `nostr_http_request()`, extract body. The LLM-specific logic (WebSocket URL detection, debug logging, status code handling) stays in llm.c — only the curl plumbing moves out.
|
||||
|
||||
Also remove:
|
||||
- `static size_t write_cb()` — replaced by nostr_http's callback
|
||||
- `static const char* detect_ca_bundle_path()` — replaced by `nostr_http_detect_ca_bundle()`
|
||||
- `typedef struct { char* data; size_t len; } response_buffer_t;` — replaced by `nostr_http_response_t`
|
||||
|
||||
### tool_local.c
|
||||
|
||||
Replace the inline curl block in `execute_local_http_fetch()` (~100 lines) with `nostr_http_request()`.
|
||||
|
||||
Also remove:
|
||||
- `static size_t local_http_fetch_write_cb_local()` — replaced
|
||||
- `static const char* detect_ca_bundle_path_for_tools_local()` — replaced
|
||||
- `typedef struct { ... } local_http_fetch_buffer_t;` — replaced
|
||||
|
||||
### cashu_wallet.c
|
||||
|
||||
Remove `detect_ca_bundle_path_for_cashu_wallet()` — replaced by `nostr_http_detect_ca_bundle()` called once at init.
|
||||
|
||||
The `cashu_mint_set_ca_bundle()` call at line 423 becomes `nostr_http_set_ca_bundle()` called once in main.c startup.
|
||||
|
||||
### Build changes
|
||||
|
||||
- Remove `#include <curl/curl.h>` from llm.c and tool_local.c
|
||||
- Didactyl only includes curl transitively through nostr_core_lib
|
||||
- CA bundle detection happens once in main.c startup via `nostr_http_set_ca_bundle(nostr_http_detect_ca_bundle())`
|
||||
|
||||
### Verification
|
||||
|
||||
After migration, grep confirms zero direct curl usage in Didactyl:
|
||||
```bash
|
||||
grep -r "curl_easy_init\|CURL\s*\*\|curl_easy_setopt" src/
|
||||
# Expected: no results
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: Blossom Client in nostr_core_lib
|
||||
|
||||
### New files
|
||||
|
||||
- `nostr_core_lib/nostr_core/blossom_client.h` — public Blossom API
|
||||
- `nostr_core_lib/nostr_core/blossom_client.c` — implementation using `nostr_http`
|
||||
|
||||
### Proposed API
|
||||
|
||||
```c
|
||||
#ifndef NOSTR_BLOSSOM_CLIENT_H
|
||||
#define NOSTR_BLOSSOM_CLIENT_H
|
||||
|
||||
#include "nostr_common.h"
|
||||
#include "../cjson/cJSON.h"
|
||||
#include <stddef.h>
|
||||
|
||||
// Blob descriptor returned by Blossom servers
|
||||
typedef struct {
|
||||
char sha256[65]; // Hex-encoded SHA-256 hash
|
||||
char url[512]; // Canonical blob URL
|
||||
long size; // Blob size in bytes
|
||||
char content_type[128]; // MIME type
|
||||
long created; // Unix timestamp
|
||||
} blossom_blob_descriptor_t;
|
||||
|
||||
// Set CA bundle for Blossom HTTP requests (delegates to nostr_http)
|
||||
void blossom_set_ca_bundle(const char* ca_bundle_path);
|
||||
|
||||
// Create a kind 24242 Blossom authorization event
|
||||
// Returns base64-encoded signed event string for Authorization header.
|
||||
// Caller must free() the returned string.
|
||||
char* blossom_create_auth_header(const unsigned char* private_key,
|
||||
const char* operation, // "upload", "delete", "list"
|
||||
const char* sha256_hex, // blob hash (NULL for list)
|
||||
int expiration_seconds);
|
||||
|
||||
// Upload file bytes to a Blossom server
|
||||
// Returns NOSTR_SUCCESS and fills descriptor on success.
|
||||
int blossom_upload(const char* server_url,
|
||||
const unsigned char* data,
|
||||
size_t data_len,
|
||||
const char* content_type,
|
||||
const unsigned char* private_key, // for auth event (NULL = no auth)
|
||||
const char* sha256_hex, // pre-computed hash (NULL = compute)
|
||||
int timeout_seconds,
|
||||
blossom_blob_descriptor_t* descriptor_out);
|
||||
|
||||
// Upload a local file to a Blossom server
|
||||
int blossom_upload_file(const char* server_url,
|
||||
const char* file_path,
|
||||
const char* content_type,
|
||||
const unsigned char* private_key,
|
||||
int timeout_seconds,
|
||||
blossom_blob_descriptor_t* descriptor_out);
|
||||
|
||||
// Download a blob by SHA-256 hash
|
||||
// Returns NOSTR_SUCCESS and fills body_out/body_len_out.
|
||||
// Caller must free(*body_out).
|
||||
int blossom_download(const char* server_url,
|
||||
const char* sha256_hex,
|
||||
int timeout_seconds,
|
||||
size_t max_bytes,
|
||||
unsigned char** body_out,
|
||||
size_t* body_len_out,
|
||||
char* content_type_out, // buffer, at least 128 bytes
|
||||
size_t content_type_out_size);
|
||||
|
||||
// Download a blob to a local file
|
||||
int blossom_download_to_file(const char* server_url,
|
||||
const char* sha256_hex,
|
||||
const char* output_path,
|
||||
int timeout_seconds,
|
||||
size_t max_bytes,
|
||||
blossom_blob_descriptor_t* descriptor_out);
|
||||
|
||||
// HEAD request — check blob existence and metadata
|
||||
int blossom_head(const char* server_url,
|
||||
const char* sha256_hex,
|
||||
int timeout_seconds,
|
||||
blossom_blob_descriptor_t* descriptor_out);
|
||||
|
||||
// Delete a blob by SHA-256 hash (requires auth)
|
||||
int blossom_delete(const char* server_url,
|
||||
const char* sha256_hex,
|
||||
const unsigned char* private_key,
|
||||
int timeout_seconds);
|
||||
|
||||
// List blobs for a pubkey
|
||||
// Returns NOSTR_SUCCESS and fills descriptors array.
|
||||
// Caller must free(*descriptors_out).
|
||||
int blossom_list(const char* server_url,
|
||||
const char* pubkey_hex,
|
||||
int timeout_seconds,
|
||||
blossom_blob_descriptor_t** descriptors_out,
|
||||
int* count_out);
|
||||
|
||||
#endif
|
||||
```
|
||||
|
||||
### Implementation details
|
||||
|
||||
- `blossom_create_auth_header()` uses `nostr_create_and_sign_event()` (kind 24242) + `base64_encode()`
|
||||
- All HTTP calls go through `nostr_http_request()` — no direct curl usage
|
||||
- `blossom_upload_file()` uses `nostr_sha256_file_stream()` to hash before upload
|
||||
- `blossom_download_to_file()` verifies SHA-256 after download
|
||||
|
||||
### Build changes
|
||||
|
||||
- Add `blossom_client.c` to nostr_core_lib build
|
||||
- Update `nostr_core.h` to include `blossom_client.h`
|
||||
- Rebuild `libnostr_core_*.a`
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: Blossom Tools in Didactyl
|
||||
|
||||
### New file
|
||||
|
||||
- `src/tools/tool_blossom.c` — thin tool wrappers calling `blossom_*()` from nostr_core_lib
|
||||
|
||||
### Tool set
|
||||
|
||||
| Tool | Description | Library Function |
|
||||
|---|---|---|
|
||||
| `blossom_upload` | Upload local file to Blossom server | `blossom_upload_file()` |
|
||||
| `blossom_download` | Download blob to local file | `blossom_download_to_file()` |
|
||||
| `blossom_head` | Check blob existence and metadata | `blossom_head()` |
|
||||
| `blossom_delete` | Delete blob from server | `blossom_delete()` |
|
||||
| `blossom_list` | List blobs by pubkey | `blossom_list()` |
|
||||
|
||||
### Tool schemas
|
||||
|
||||
(Unchanged from original plan — see Tool Contracts section below.)
|
||||
|
||||
### Integration points
|
||||
|
||||
1. **`tools_internal.h`** — add `execute_blossom_*()` prototypes
|
||||
2. **`tools_dispatch.c`** — add `strcmp` branches
|
||||
3. **`tools_schema.c`** — add OpenAI tool definitions
|
||||
4. **`Makefile`** — add `$(SRC_DIR)/tools/tool_blossom.c` to SRCS
|
||||
5. **`docs/TOOLS.md`** — add Blossom Storage Tools section
|
||||
|
||||
### Pattern
|
||||
|
||||
Follows the exact same pattern as Cashu:
|
||||
|
||||
```
|
||||
tool_blossom.c (arg parsing + JSON result formatting)
|
||||
→ blossom_client.h (nostr_core_lib - domain API)
|
||||
→ nostr_http.h (nostr_core_lib - shared HTTP client)
|
||||
```
|
||||
|
||||
Just like:
|
||||
|
||||
```
|
||||
tool_cashu_wallet.c (arg parsing + JSON result formatting)
|
||||
→ cashu_wallet.c (Didactyl - wallet state + Nostr persistence)
|
||||
→ cashu_mint.h (nostr_core_lib - domain API)
|
||||
→ nostr_http.h (nostr_core_lib - shared HTTP client)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Tool Contracts
|
||||
|
||||
All tools return a JSON object with at minimum:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"error": "...optional on failure..."
|
||||
}
|
||||
```
|
||||
|
||||
### `blossom_upload`
|
||||
|
||||
**Input:**
|
||||
```json
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"server": { "type": "string", "description": "Blossom server base URL; omit if default configured" },
|
||||
"file_path": { "type": "string", "description": "Relative local path inside working directory" },
|
||||
"content_type": { "type": "string", "description": "Optional MIME type override" }
|
||||
},
|
||||
"required": ["file_path"]
|
||||
}
|
||||
```
|
||||
|
||||
**Output:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"server": "https://blossom.example",
|
||||
"sha256": "<64-hex>",
|
||||
"size": 12345,
|
||||
"content_type": "image/png",
|
||||
"url": "https://blossom.example/<sha256>"
|
||||
}
|
||||
```
|
||||
|
||||
### `blossom_download`
|
||||
|
||||
**Input:**
|
||||
```json
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"server": { "type": "string" },
|
||||
"sha256": { "type": "string", "description": "Blob hash hex identifier" },
|
||||
"url": { "type": "string", "description": "Direct blob URL if server+sha256 not provided" },
|
||||
"output_path": { "type": "string", "description": "Relative path to write file" },
|
||||
"overwrite": { "type": "boolean", "default": false }
|
||||
},
|
||||
"required": ["output_path"]
|
||||
}
|
||||
```
|
||||
|
||||
**Output:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"output_path": "downloads/file.bin",
|
||||
"bytes_written": 12345,
|
||||
"sha256": "<computed-64-hex>",
|
||||
"verified": true,
|
||||
"content_type": "application/octet-stream"
|
||||
}
|
||||
```
|
||||
|
||||
### `blossom_head`
|
||||
|
||||
**Input:**
|
||||
```json
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"server": { "type": "string" },
|
||||
"sha256": { "type": "string" },
|
||||
"url": { "type": "string" }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Output:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"exists": true,
|
||||
"sha256": "<64-hex>",
|
||||
"size": 12345,
|
||||
"content_type": "image/jpeg"
|
||||
}
|
||||
```
|
||||
|
||||
### `blossom_delete`
|
||||
|
||||
**Input:**
|
||||
```json
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"server": { "type": "string" },
|
||||
"sha256": { "type": "string" }
|
||||
},
|
||||
"required": ["sha256"]
|
||||
}
|
||||
```
|
||||
|
||||
**Output:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"deleted": true,
|
||||
"server": "https://blossom.example",
|
||||
"sha256": "<64-hex>"
|
||||
}
|
||||
```
|
||||
|
||||
### `blossom_list`
|
||||
|
||||
**Input:**
|
||||
```json
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"server": { "type": "string" },
|
||||
"pubkey": { "type": "string", "description": "Hex pubkey; defaults to agent pubkey" }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Output:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"server": "https://blossom.example",
|
||||
"pubkey": "<64-hex>",
|
||||
"blobs": [
|
||||
{ "sha256": "...", "size": 12345, "content_type": "image/png", "url": "...", "created": 1679000000 }
|
||||
],
|
||||
"count": 1
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Validation and Safety Rules
|
||||
|
||||
1. `server` must be `https://` unless explicit config allows insecure local testing
|
||||
2. `sha256` must match `^[0-9a-fA-F]{64}$`
|
||||
3. `file_path` and `output_path` must be safe relative paths (reuse `tool_local.c` pattern)
|
||||
4. Enforce maximum upload/download size from config
|
||||
5. Refuse overwrite unless `overwrite=true`
|
||||
6. Normalize all errors into `{"success": false, "error": "..."}`
|
||||
7. Do not leak keys/secrets in returned payload
|
||||
|
||||
---
|
||||
|
||||
## Execution Order
|
||||
|
||||
### Step 1: `nostr_http` in nostr_core_lib
|
||||
- Create `nostr_http.h` and `nostr_http.c`
|
||||
- Write unit tests for GET, POST, PUT, DELETE, HEAD
|
||||
- Verify CA bundle detection works across distros
|
||||
|
||||
### Step 2: Migrate nostr_core_lib consumers
|
||||
- Refactor `cashu_mint.c` to use `nostr_http`
|
||||
- Refactor `nip005.c` to use `nostr_http`
|
||||
- Refactor `nip011.c` to use `nostr_http`
|
||||
- Remove direct `#include <curl/curl.h>` from all three
|
||||
- Run existing tests to verify no regressions
|
||||
|
||||
### Step 3: Migrate Didactyl consumers
|
||||
- Refactor `llm.c` to use `nostr_http`
|
||||
- Refactor `tool_local.c` to use `nostr_http`
|
||||
- Update `cashu_wallet.c` CA bundle init
|
||||
- Remove all direct curl includes from Didactyl src/
|
||||
- Verify: `grep -r "curl_easy_init" src/` returns zero results
|
||||
- Run existing tests
|
||||
|
||||
### Step 4: `blossom_client` in nostr_core_lib
|
||||
- Create `blossom_client.h` and `blossom_client.c`
|
||||
- Implement auth event builder, upload, download, head, delete, list
|
||||
- Write unit tests
|
||||
|
||||
### Step 5: Blossom tools in Didactyl
|
||||
- Create `tool_blossom.c`
|
||||
- Add schemas, dispatch, prototypes
|
||||
- Update docs
|
||||
- Run full test suite
|
||||
|
||||
---
|
||||
|
||||
## Architecture Diagram
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
subgraph "Didactyl Tools Layer"
|
||||
TB[tool_blossom.c]
|
||||
TCW[tool_cashu_wallet.c]
|
||||
TL[tool_local.c]
|
||||
LLM[llm.c]
|
||||
end
|
||||
|
||||
subgraph "Didactyl Domain Layer"
|
||||
CW[cashu_wallet.c]
|
||||
end
|
||||
|
||||
subgraph "nostr_core_lib - Domain Clients"
|
||||
BC[blossom_client.c]
|
||||
CM[cashu_mint.c]
|
||||
N05[nip005.c]
|
||||
N11[nip011.c]
|
||||
end
|
||||
|
||||
subgraph "nostr_core_lib - Shared Infrastructure"
|
||||
NH[nostr_http.c<br/>Single curl implementation]
|
||||
NIP1[nip001.c<br/>Event signing]
|
||||
UTIL[utils.c<br/>SHA-256 + base64 + hex]
|
||||
end
|
||||
|
||||
TB --> BC
|
||||
TCW --> CW
|
||||
CW --> CM
|
||||
TL --> NH
|
||||
LLM --> NH
|
||||
|
||||
BC --> NH
|
||||
BC --> NIP1
|
||||
BC --> UTIL
|
||||
CM --> NH
|
||||
N05 --> NH
|
||||
N11 --> NH
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing Plan
|
||||
|
||||
### Unit tests
|
||||
1. `nostr_http` — GET/POST/PUT/DELETE/HEAD, timeouts, max_response_bytes, CA bundle
|
||||
2. `blossom_client` — auth event creation, upload/download/head/delete/list
|
||||
3. Argument parsing and validation for all Blossom tools
|
||||
|
||||
### Integration tests
|
||||
- Mock Blossom server for upload/download/head/delete/list
|
||||
- Verify SHA-256 integrity on download
|
||||
- Test auth event expiration
|
||||
- Test error responses (404, 401, 403, 500)
|
||||
|
||||
### Regression tests
|
||||
- All existing Cashu wallet tests pass after cashu_mint migration
|
||||
- All existing NIP-05 and NIP-11 tests pass
|
||||
- LLM requests work correctly after llm.c migration
|
||||
- `local_http_fetch` tool works correctly after tool_local.c migration
|
||||
|
||||
---
|
||||
|
||||
## Risks and Mitigations
|
||||
|
||||
1. **nostr_core_lib API change breaks Didactyl build**
|
||||
- Mitigation: version-pin nostr_core_lib; test both projects together before release
|
||||
|
||||
2. **Subtle curl behavior differences after migration**
|
||||
- Mitigation: keep convenience wrappers thin; run existing test suites at each step
|
||||
|
||||
3. **Binary body support gaps**
|
||||
- Mitigation: `nostr_http_request_t.body` is `unsigned char*` with explicit length from day one
|
||||
|
||||
4. **CA bundle detection regression on specific distros**
|
||||
- Mitigation: consolidate all 5 existing detection paths into one comprehensive function
|
||||
|
||||
5. **Blossom server protocol variance**
|
||||
- Mitigation: defensive response parsing; test against multiple server implementations
|
||||
|
||||
---
|
||||
|
||||
## Files Changed Summary
|
||||
|
||||
### nostr_core_lib (new)
|
||||
- `nostr_core/nostr_http.h` — shared HTTP client API
|
||||
- `nostr_core/nostr_http.c` — shared HTTP client implementation
|
||||
- `nostr_core/blossom_client.h` — Blossom client API
|
||||
- `nostr_core/blossom_client.c` — Blossom client implementation
|
||||
|
||||
### nostr_core_lib (modified)
|
||||
- `nostr_core/cashu_mint.c` — replace static curl with `nostr_http`
|
||||
- `nostr_core/nip005.c` — replace static curl with `nostr_http`
|
||||
- `nostr_core/nip011.c` — replace static curl with `nostr_http`
|
||||
- `nostr_core/nostr_core.h` — add includes for new headers
|
||||
- `build.sh` — add new source files
|
||||
|
||||
### Didactyl (new)
|
||||
- `src/tools/tool_blossom.c` — Blossom tool implementations
|
||||
|
||||
### Didactyl (modified)
|
||||
- `src/llm.c` — replace static curl with `nostr_http`
|
||||
- `src/tools/tool_local.c` — replace static curl with `nostr_http`
|
||||
- `src/cashu_wallet.c` — update CA bundle init
|
||||
- `src/main.c` — add `nostr_http_set_ca_bundle()` call at startup
|
||||
- `src/tools/tools_internal.h` — add Blossom prototypes
|
||||
- `src/tools/tools_dispatch.c` — add Blossom dispatch
|
||||
- `src/tools/tools_schema.c` — add Blossom schemas
|
||||
- `Makefile` — add `tool_blossom.c` to SRCS
|
||||
- `docs/TOOLS.md` — add Blossom section
|
||||
+1
-27
@@ -10,7 +10,6 @@
|
||||
#include <time.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#include <unistd.h>
|
||||
|
||||
#include "debug.h"
|
||||
#include "nostr_handler.h"
|
||||
@@ -45,27 +44,6 @@ typedef struct {
|
||||
|
||||
static cashu_wallet_state_t g_wallet = {0};
|
||||
|
||||
static const char* detect_ca_bundle_path_for_cashu_wallet(void) {
|
||||
const char* env = getenv("SSL_CERT_FILE");
|
||||
if (env && env[0] != '\0' && access(env, R_OK) == 0) {
|
||||
return env;
|
||||
}
|
||||
|
||||
static const char* candidates[] = {
|
||||
"/etc/ssl/certs/ca-certificates.crt", // Debian/Ubuntu
|
||||
"/etc/ssl/cert.pem", // Alpine
|
||||
"/etc/pki/tls/certs/ca-bundle.crt", // RHEL/CentOS/Fedora
|
||||
"/etc/ssl/ca-bundle.pem" // openSUSE
|
||||
};
|
||||
|
||||
for (size_t i = 0; i < sizeof(candidates) / sizeof(candidates[0]); i++) {
|
||||
if (access(candidates[i], R_OK) == 0) {
|
||||
return candidates[i];
|
||||
}
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void wallet_clear_tokens_locked(void) {
|
||||
if (!g_wallet.tokens) {
|
||||
@@ -419,7 +397,7 @@ int cashu_wallet_init(didactyl_config_t* cfg) {
|
||||
g_wallet.initialized = 1;
|
||||
}
|
||||
|
||||
const char* ca_bundle = detect_ca_bundle_path_for_cashu_wallet();
|
||||
const char* ca_bundle = nostr_http_detect_ca_bundle();
|
||||
cashu_mint_set_ca_bundle(ca_bundle);
|
||||
|
||||
pthread_mutex_lock(&g_wallet.mutex);
|
||||
@@ -680,10 +658,6 @@ int cashu_wallet_load_from_relays(void) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (nostr_block_list_is_event_blocked(id->valuestring)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
nostr_nip60_token_data_t token;
|
||||
memset(&token, 0, sizeof(token));
|
||||
if (nostr_nip60_parse_token_event(ev, g_wallet.cfg->keys.private_key, &token) != 0) {
|
||||
|
||||
@@ -230,6 +230,10 @@ static int parse_tools_config(cJSON* root, didactyl_config_t* config) {
|
||||
cJSON_GetObjectItemCaseSensitive(tools, "local_http_fetch_default_timeout_seconds");
|
||||
cJSON* local_http_fetch_max_timeout_seconds =
|
||||
cJSON_GetObjectItemCaseSensitive(tools, "local_http_fetch_max_timeout_seconds");
|
||||
cJSON* blossom_max_upload_bytes =
|
||||
cJSON_GetObjectItemCaseSensitive(tools, "blossom_max_upload_bytes");
|
||||
cJSON* blossom_max_download_bytes =
|
||||
cJSON_GetObjectItemCaseSensitive(tools, "blossom_max_download_bytes");
|
||||
if (enabled && cJSON_IsBool(enabled)) {
|
||||
config->tools.enabled = cJSON_IsTrue(enabled) ? 1 : 0;
|
||||
}
|
||||
@@ -256,6 +260,12 @@ static int parse_tools_config(cJSON* root, didactyl_config_t* config) {
|
||||
config->tools.local_http_fetch_max_timeout_seconds =
|
||||
(int)local_http_fetch_max_timeout_seconds->valuedouble;
|
||||
}
|
||||
if (blossom_max_upload_bytes && cJSON_IsNumber(blossom_max_upload_bytes)) {
|
||||
config->tools.blossom_max_upload_bytes = (int)blossom_max_upload_bytes->valuedouble;
|
||||
}
|
||||
if (blossom_max_download_bytes && cJSON_IsNumber(blossom_max_download_bytes)) {
|
||||
config->tools.blossom_max_download_bytes = (int)blossom_max_download_bytes->valuedouble;
|
||||
}
|
||||
|
||||
cJSON* shell = cJSON_GetObjectItemCaseSensitive(tools, "shell");
|
||||
if (!shell || !cJSON_IsObject(shell)) {
|
||||
@@ -310,6 +320,12 @@ static int parse_tools_config(cJSON* root, didactyl_config_t* config) {
|
||||
if (config->tools.local_http_fetch_default_timeout_seconds > config->tools.local_http_fetch_max_timeout_seconds) {
|
||||
config->tools.local_http_fetch_default_timeout_seconds = config->tools.local_http_fetch_max_timeout_seconds;
|
||||
}
|
||||
if (config->tools.blossom_max_upload_bytes < 1024) {
|
||||
config->tools.blossom_max_upload_bytes = 16 * 1024 * 1024;
|
||||
}
|
||||
if (config->tools.blossom_max_download_bytes < 1024) {
|
||||
config->tools.blossom_max_download_bytes = 16 * 1024 * 1024;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -1231,6 +1247,8 @@ int config_load(const char* path, didactyl_config_t* config) {
|
||||
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.blossom_max_upload_bytes = 16 * 1024 * 1024;
|
||||
config->tools.blossom_max_download_bytes = 16 * 1024 * 1024;
|
||||
config->tools.shell.enabled = 1;
|
||||
config->tools.shell.timeout_seconds = 30;
|
||||
config->tools.shell.max_output_bytes = 65536;
|
||||
|
||||
@@ -54,6 +54,8 @@ typedef struct {
|
||||
int local_http_fetch_default_timeout_seconds;
|
||||
int local_http_fetch_max_timeout_seconds;
|
||||
shell_tools_config_t shell;
|
||||
int blossom_max_upload_bytes;
|
||||
int blossom_max_download_bytes;
|
||||
} tools_config_t;
|
||||
|
||||
typedef struct {
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
#include "llm.h"
|
||||
|
||||
#include <curl/curl.h>
|
||||
#include <ctype.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
@@ -12,60 +11,11 @@
|
||||
#include "cjson/cJSON.h"
|
||||
#include "debug.h"
|
||||
|
||||
typedef struct {
|
||||
char* data;
|
||||
size_t len;
|
||||
size_t cap;
|
||||
} response_buffer_t;
|
||||
#include "../nostr_core_lib/nostr_core/nostr_http.h"
|
||||
|
||||
static llm_config_t g_cfg;
|
||||
static int g_initialized = 0;
|
||||
|
||||
static size_t write_cb(void* contents, size_t size, size_t nmemb, void* userp) {
|
||||
response_buffer_t* rb = (response_buffer_t*)userp;
|
||||
size_t total = size * nmemb;
|
||||
|
||||
if (rb->len + total + 1U > rb->cap) {
|
||||
size_t new_cap = rb->cap == 0 ? 1024U : rb->cap * 2U;
|
||||
while (new_cap < rb->len + total + 1U) {
|
||||
new_cap *= 2U;
|
||||
}
|
||||
char* p = (char*)realloc(rb->data, new_cap);
|
||||
if (!p) {
|
||||
return 0;
|
||||
}
|
||||
rb->data = p;
|
||||
rb->cap = new_cap;
|
||||
}
|
||||
|
||||
memcpy(rb->data + rb->len, contents, total);
|
||||
rb->len += total;
|
||||
rb->data[rb->len] = '\0';
|
||||
return total;
|
||||
}
|
||||
|
||||
static const char* detect_ca_bundle_path(void) {
|
||||
const char* env = getenv("SSL_CERT_FILE");
|
||||
if (env && env[0] != '\0' && access(env, R_OK) == 0) {
|
||||
return env;
|
||||
}
|
||||
|
||||
static const char* candidates[] = {
|
||||
"/etc/ssl/certs/ca-certificates.crt", // Debian/Ubuntu
|
||||
"/etc/ssl/cert.pem", // Alpine
|
||||
"/etc/pki/tls/certs/ca-bundle.crt", // RHEL/CentOS/Fedora
|
||||
"/etc/ssl/ca-bundle.pem" // openSUSE
|
||||
};
|
||||
|
||||
for (size_t i = 0; i < sizeof(candidates) / sizeof(candidates[0]); i++) {
|
||||
if (access(candidates[i], R_OK) == 0) {
|
||||
return candidates[i];
|
||||
}
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static int url_looks_like_websocket(const char* url) {
|
||||
if (!url) return 0;
|
||||
return (strncmp(url, "ws://", 5) == 0) || (strncmp(url, "wss://", 6) == 0);
|
||||
@@ -86,9 +36,7 @@ static int json_string_is_blank(const cJSON* item) {
|
||||
}
|
||||
|
||||
static char* perform_http_request(const char* url, const char* body, int is_post) {
|
||||
CURL* curl = curl_easy_init();
|
||||
if (!curl || !url) {
|
||||
if (curl) curl_easy_cleanup(curl);
|
||||
if (!url) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
@@ -96,33 +44,30 @@ static char* perform_http_request(const char* url, const char* body, int is_post
|
||||
DEBUG_ERROR("[didactyl] llm config error: base_url must be HTTP(S), got WebSocket URL: %s",
|
||||
url);
|
||||
DEBUG_WARN("[didactyl] llm hint: set llm.base_url to an OpenAI-compatible HTTPS endpoint, e.g. https://api.example.com/v1");
|
||||
curl_easy_cleanup(curl);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
||||
response_buffer_t rb = {0};
|
||||
struct curl_slist* headers = NULL;
|
||||
headers = curl_slist_append(headers, "Content-Type: application/json");
|
||||
|
||||
char auth_header[OW_MAX_KEY_LEN + 32];
|
||||
snprintf(auth_header, sizeof(auth_header), "Authorization: Bearer %s", g_cfg.api_key);
|
||||
headers = curl_slist_append(headers, auth_header);
|
||||
const char* headers[] = {
|
||||
"Content-Type: application/json",
|
||||
auth_header,
|
||||
NULL
|
||||
};
|
||||
|
||||
curl_easy_setopt(curl, CURLOPT_URL, url);
|
||||
curl_easy_setopt(curl, CURLOPT_HTTPGET, is_post ? 0L : 1L);
|
||||
nostr_http_request_t req;
|
||||
memset(&req, 0, sizeof(req));
|
||||
req.method = is_post ? "POST" : "GET";
|
||||
req.url = url;
|
||||
req.headers = headers;
|
||||
req.timeout_seconds = 60;
|
||||
req.follow_redirects = 1;
|
||||
req.max_redirects = 3;
|
||||
req.user_agent = "didactyl/llm";
|
||||
if (is_post) {
|
||||
curl_easy_setopt(curl, CURLOPT_POST, 1L);
|
||||
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body ? body : "{}");
|
||||
}
|
||||
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 60L);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_cb);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &rb);
|
||||
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
|
||||
|
||||
const char* ca_bundle = detect_ca_bundle_path();
|
||||
if (ca_bundle) {
|
||||
curl_easy_setopt(curl, CURLOPT_CAINFO, ca_bundle);
|
||||
const char* payload = body ? body : "{}";
|
||||
req.body = (const unsigned char*)payload;
|
||||
req.body_len = strlen(payload);
|
||||
}
|
||||
|
||||
if (is_post) {
|
||||
@@ -137,44 +82,38 @@ static char* perform_http_request(const char* url, const char* body, int is_post
|
||||
DEBUG_INFO("[didactyl] llm request: method=GET url=%s", url);
|
||||
}
|
||||
|
||||
CURLcode res = curl_easy_perform(curl);
|
||||
long status = 0;
|
||||
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &status);
|
||||
|
||||
curl_slist_free_all(headers);
|
||||
curl_easy_cleanup(curl);
|
||||
|
||||
if (res != CURLE_OK) {
|
||||
DEBUG_ERROR("[didactyl] llm http request failed: curl=%s", curl_easy_strerror(res));
|
||||
if (rb.data && rb.len > 0) {
|
||||
DEBUG_WARN("[didactyl] llm partial response: %.600s%s",
|
||||
rb.data,
|
||||
rb.len > 600 ? "..." : "");
|
||||
}
|
||||
free(rb.data);
|
||||
nostr_http_response_t resp;
|
||||
int rc = nostr_http_request(&req, &resp);
|
||||
if (rc != NOSTR_SUCCESS) {
|
||||
DEBUG_ERROR("[didactyl] llm http request failed: transport error rc=%d", rc);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (status < 200 || status >= 300) {
|
||||
DEBUG_ERROR("[didactyl] llm http request failed: status=%ld", status);
|
||||
if (status == 101) {
|
||||
if (resp.status_code < 200 || resp.status_code >= 300) {
|
||||
DEBUG_ERROR("[didactyl] llm http request failed: status=%ld", resp.status_code);
|
||||
if (resp.status_code == 101) {
|
||||
DEBUG_WARN("[didactyl] llm hint: received HTTP 101 (Switching Protocols), which usually means llm.base_url points to a WebSocket server instead of an HTTP LLM API");
|
||||
}
|
||||
if (rb.data && rb.len > 0) {
|
||||
if (resp.body && resp.body_len > 0) {
|
||||
DEBUG_WARN("[didactyl] llm error response: %.1200s%s",
|
||||
rb.data,
|
||||
rb.len > 1200 ? "..." : "");
|
||||
resp.body,
|
||||
resp.body_len > 1200 ? "..." : "");
|
||||
}
|
||||
free(rb.data);
|
||||
nostr_http_response_free(&resp);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (!rb.data) {
|
||||
if (!resp.body) {
|
||||
DEBUG_ERROR("[didactyl] llm http request failed: empty response body");
|
||||
nostr_http_response_free(&resp);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return rb.data;
|
||||
char* out = resp.body;
|
||||
free(resp.content_type);
|
||||
free(resp.headers_raw);
|
||||
memset(&resp, 0, sizeof(resp));
|
||||
return out;
|
||||
}
|
||||
|
||||
static char* perform_chat_request(const char* body) {
|
||||
@@ -315,7 +254,6 @@ int llm_init(const llm_config_t* config) {
|
||||
}
|
||||
memset(&g_cfg, 0, sizeof(g_cfg));
|
||||
g_cfg = *config;
|
||||
curl_global_init(CURL_GLOBAL_DEFAULT);
|
||||
g_initialized = 1;
|
||||
return 0;
|
||||
}
|
||||
@@ -512,7 +450,6 @@ void llm_cleanup(void) {
|
||||
if (!g_initialized) {
|
||||
return;
|
||||
}
|
||||
curl_global_cleanup();
|
||||
memset(&g_cfg, 0, sizeof(g_cfg));
|
||||
g_initialized = 0;
|
||||
}
|
||||
|
||||
+22
-6
@@ -7,7 +7,7 @@
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "../../nostr_core_lib/nostr_core/nostr_core.h"
|
||||
#include "../nostr_core_lib/nostr_core/nostr_core.h"
|
||||
#include "main.h"
|
||||
#include "agent.h"
|
||||
#include "config.h"
|
||||
@@ -1066,6 +1066,13 @@ int main(int argc, char** argv) {
|
||||
snprintf(cfg.api.bind_address, sizeof(cfg.api.bind_address), "%s", api_bind_override);
|
||||
}
|
||||
|
||||
{
|
||||
const char* ca_bundle = nostr_http_detect_ca_bundle();
|
||||
if (ca_bundle && ca_bundle[0] != '\0') {
|
||||
nostr_http_set_ca_bundle(ca_bundle);
|
||||
}
|
||||
}
|
||||
|
||||
if (dump_schemas || test_tool_name) {
|
||||
if (llm_init(&cfg.llm) != 0) {
|
||||
fprintf(stderr, "Failed to initialize llm client\n");
|
||||
@@ -1102,6 +1109,7 @@ int main(int argc, char** argv) {
|
||||
if (cashu_wallet_init(&cfg) == 0) {
|
||||
wallet_initialized = 1;
|
||||
if (cfg.cashu_wallet.enabled) {
|
||||
(void)nostr_handler_subscribe_wallet_events();
|
||||
int wallet_rc = 0;
|
||||
if (cfg.cashu_wallet.auto_load) {
|
||||
wallet_rc = cashu_wallet_load_from_relays();
|
||||
@@ -1420,10 +1428,18 @@ int main(int argc, char** argv) {
|
||||
DEBUG_INFO("[didactyl] startup phase: subscribe DMs end");
|
||||
startup_step_ok(15, "Subscribe DMs", NULL);
|
||||
|
||||
startup_step_begin(16, "Initialize cashu wallet");
|
||||
startup_step_begin(16, "Subscribe wallet events");
|
||||
DEBUG_INFO("[didactyl] startup phase: subscribe wallet events begin");
|
||||
if (nostr_handler_subscribe_wallet_events() != 0) {
|
||||
DEBUG_WARN("[didactyl] startup phase: subscribe wallet events failed (continuing)");
|
||||
}
|
||||
DEBUG_INFO("[didactyl] startup phase: subscribe wallet events end");
|
||||
startup_step_ok(16, "Subscribe wallet events", NULL);
|
||||
|
||||
startup_step_begin(17, "Initialize cashu wallet");
|
||||
if (cashu_wallet_init(&cfg) != 0) {
|
||||
DEBUG_WARN("[didactyl] startup phase: cashu_wallet_init failed (continuing without wallet)");
|
||||
startup_step_fail(16, "Initialize cashu wallet", "cashu_wallet_init failed; wallet tools may return errors");
|
||||
startup_step_fail(17, "Initialize cashu wallet", "cashu_wallet_init failed; wallet tools may return errors");
|
||||
} else {
|
||||
int wallet_rc = 0;
|
||||
if (cfg.cashu_wallet.auto_load) {
|
||||
@@ -1434,9 +1450,9 @@ int main(int argc, char** argv) {
|
||||
}
|
||||
if (wallet_rc != 0) {
|
||||
DEBUG_WARN("[didactyl] startup phase: cashu wallet load/create deferred to first tool call");
|
||||
startup_step_ok(16, "Initialize cashu wallet", "initialized; load/create deferred");
|
||||
startup_step_ok(17, "Initialize cashu wallet", "initialized; load/create deferred");
|
||||
} else {
|
||||
startup_step_ok(16, "Initialize cashu wallet", cfg.cashu_wallet.auto_load ? "loaded or created" : "initialized");
|
||||
startup_step_ok(17, "Initialize cashu wallet", cfg.cashu_wallet.auto_load ? "loaded or created" : "initialized");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1476,7 +1492,7 @@ int main(int argc, char** argv) {
|
||||
|
||||
nostr_handler_refresh_relay_statuses();
|
||||
|
||||
startup_step_ok(17, "READY", "agent online; entering main poll loop");
|
||||
startup_step_ok(18, "READY", "agent online; entering main poll loop");
|
||||
DEBUG_INFO("[didactyl] entering main poll loop");
|
||||
DEBUG_INFO("[didactyl] running with pubkey %s", cfg.keys.public_key_hex);
|
||||
|
||||
|
||||
+2
-2
@@ -12,8 +12,8 @@
|
||||
// Using DIDACTYL_ prefix to avoid conflicts with nostr_core_lib VERSION macros
|
||||
#define DIDACTYL_VERSION_MAJOR 0
|
||||
#define DIDACTYL_VERSION_MINOR 1
|
||||
#define DIDACTYL_VERSION_PATCH 19
|
||||
#define DIDACTYL_VERSION "v0.1.19"
|
||||
#define DIDACTYL_VERSION_PATCH 21
|
||||
#define DIDACTYL_VERSION "v0.1.21"
|
||||
|
||||
// Agent metadata
|
||||
#define DIDACTYL_NAME "Didactyl"
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#include "debug.h"
|
||||
#include "trigger_manager.h"
|
||||
#include "nostr_block_list.h"
|
||||
#include "cashu_wallet.h"
|
||||
|
||||
#define NIP17_MAX_RELAYS 32
|
||||
#define NIP17_MAX_GIFT_WRAPS 8
|
||||
@@ -61,6 +62,7 @@ typedef enum {
|
||||
MANAGED_SUB_ADMIN_SKILLS,
|
||||
MANAGED_SUB_DM_KIND4,
|
||||
MANAGED_SUB_DM_KIND1059,
|
||||
MANAGED_SUB_WALLET,
|
||||
MANAGED_SUB_COUNT
|
||||
} managed_subscription_id_t;
|
||||
|
||||
@@ -404,6 +406,7 @@ static void load_startup_display_name(void);
|
||||
static void build_startup_kind1_content(char* out, size_t out_size, const char* fallback);
|
||||
static void register_trigger_from_self_skill_event(cJSON* event);
|
||||
static void on_self_skill_event(cJSON* event, const char* relay_url, void* user_data);
|
||||
static void on_wallet_event(cJSON* event, const char* relay_url, void* user_data);
|
||||
static void dm_history_clear_locked(void);
|
||||
static void dm_history_remember(const char* peer_pubkey_hex, const char* content, int incoming, time_t created_at);
|
||||
static int relay_list_contains_local(char** relays, int relay_count, const char* relay_url);
|
||||
@@ -591,6 +594,7 @@ static void managed_subscriptions_init_defaults(void) {
|
||||
g_managed_subs[MANAGED_SUB_ADMIN_SKILLS].name = "admin_skills";
|
||||
g_managed_subs[MANAGED_SUB_DM_KIND4].name = "dms_kind4";
|
||||
g_managed_subs[MANAGED_SUB_DM_KIND1059].name = "dms_kind1059";
|
||||
g_managed_subs[MANAGED_SUB_WALLET].name = "wallet_events";
|
||||
|
||||
g_managed_subs_initialized = 1;
|
||||
}
|
||||
@@ -2158,6 +2162,13 @@ static void on_self_skill_event(cJSON* event, const char* relay_url, void* user_
|
||||
pthread_mutex_unlock(&g_self_skill_mutex);
|
||||
}
|
||||
|
||||
static void on_wallet_event(cJSON* event, const char* relay_url, void* user_data) {
|
||||
(void)event;
|
||||
(void)relay_url;
|
||||
(void)user_data;
|
||||
(void)cashu_wallet_load_from_relays();
|
||||
}
|
||||
|
||||
int nostr_handler_init(didactyl_config_t* config) {
|
||||
if (!config) {
|
||||
return -1;
|
||||
@@ -2634,6 +2645,57 @@ int nostr_handler_subscribe_dms(dm_callback_t callback, void* user_data) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int nostr_handler_subscribe_wallet_events(void) {
|
||||
if (!g_cfg || !g_pool) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
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(NOSTR_NIP60_WALLET_KIND));
|
||||
cJSON_AddItemToArray(kinds, cJSON_CreateNumber(NOSTR_NIP60_TOKEN_KIND));
|
||||
cJSON_AddItemToArray(kinds, cJSON_CreateNumber(5));
|
||||
cJSON_AddItemToObject(filter, "kinds", kinds);
|
||||
cJSON_AddItemToArray(authors, cJSON_CreateString(g_cfg->keys.public_key_hex));
|
||||
cJSON_AddItemToObject(filter, "authors", authors);
|
||||
cJSON_AddNumberToObject(filter, "since", (double)g_start_time);
|
||||
cJSON_AddNumberToObject(filter, "limit", 500);
|
||||
|
||||
int rc = 0;
|
||||
pthread_mutex_lock(&g_subscription_mutex);
|
||||
managed_subscriptions_init_defaults();
|
||||
if (managed_subscription_register_locked(MANAGED_SUB_WALLET,
|
||||
filter,
|
||||
on_wallet_event,
|
||||
NULL,
|
||||
NULL,
|
||||
0,
|
||||
0,
|
||||
NOSTR_POOL_EOSE_FULL_SET,
|
||||
30,
|
||||
120,
|
||||
1) != 0) {
|
||||
rc = -1;
|
||||
}
|
||||
pthread_mutex_unlock(&g_subscription_mutex);
|
||||
cJSON_Delete(filter);
|
||||
|
||||
if (rc != 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
DEBUG_INFO("[didactyl] wallet event subscription active for pubkey %.16s...", g_cfg->keys.public_key_hex);
|
||||
return 0;
|
||||
}
|
||||
|
||||
void nostr_handler_set_self_skill_eose_callback(nostr_self_skill_eose_cb_t callback, void* user_data) {
|
||||
g_self_skill_eose_cb = callback;
|
||||
g_self_skill_eose_user_data = user_data;
|
||||
|
||||
@@ -40,6 +40,7 @@ int nostr_handler_subscribe_self_skills(void);
|
||||
char* nostr_handler_get_self_events_by_kind_json(int kind);
|
||||
void nostr_handler_set_self_skill_eose_callback(nostr_self_skill_eose_cb_t callback, void* user_data);
|
||||
int nostr_handler_subscribe_dms(dm_callback_t callback, void* user_data);
|
||||
int nostr_handler_subscribe_wallet_events(void);
|
||||
int nostr_handler_send_dm(const char* recipient_pubkey_hex, const char* message);
|
||||
int nostr_handler_send_dm_auto(const char* recipient_pubkey_hex, const char* message);
|
||||
int nostr_handler_publish_kind_event(int kind, const char* content, cJSON* tags, nostr_publish_result_t* out_result);
|
||||
|
||||
@@ -278,6 +278,8 @@ static void config_set_defaults(didactyl_config_t* cfg) {
|
||||
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.blossom_max_upload_bytes = 16 * 1024 * 1024;
|
||||
cfg->tools.blossom_max_download_bytes = 16 * 1024 * 1024;
|
||||
cfg->tools.shell.enabled = 1;
|
||||
cfg->tools.shell.timeout_seconds = 30;
|
||||
cfg->tools.shell.max_output_bytes = 65536;
|
||||
|
||||
@@ -0,0 +1,304 @@
|
||||
#define _POSIX_C_SOURCE 200809L
|
||||
|
||||
#include "tools_internal.h"
|
||||
|
||||
#include <limits.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "cjson/cJSON.h"
|
||||
#include "../../nostr_core_lib/nostr_core/blossom_client.h"
|
||||
|
||||
static char* json_error_local(const char* msg) {
|
||||
cJSON* root = cJSON_CreateObject();
|
||||
if (!root) return NULL;
|
||||
cJSON_AddBoolToObject(root, "success", 0);
|
||||
cJSON_AddStringToObject(root, "error", msg ? msg : "unknown error");
|
||||
char* out = cJSON_PrintUnformatted(root);
|
||||
cJSON_Delete(root);
|
||||
return out;
|
||||
}
|
||||
|
||||
static cJSON* parse_args_local(const char* args_json) {
|
||||
const char* raw = args_json ? args_json : "{}";
|
||||
cJSON* args = cJSON_Parse(raw);
|
||||
if (!args || !cJSON_IsObject(args)) {
|
||||
cJSON_Delete(args);
|
||||
return NULL;
|
||||
}
|
||||
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 = (strcmp(cwd, ".") == 0) ? snprintf(out, out_size, "%s", rel_path)
|
||||
: snprintf(out, out_size, "%s/%s", cwd, rel_path);
|
||||
if (n < 0 || (size_t)n >= out_size) return -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int is_https_server_local(const char* server) {
|
||||
return server && strncmp(server, "https://", 8) == 0;
|
||||
}
|
||||
|
||||
static int file_exists_local(const char* path) {
|
||||
if (!path || path[0] == '\0') return 0;
|
||||
return access(path, F_OK) == 0;
|
||||
}
|
||||
|
||||
static int file_size_local(const char* path, size_t* out_size) {
|
||||
if (!path || !out_size) return -1;
|
||||
struct stat st;
|
||||
if (stat(path, &st) != 0) return -1;
|
||||
if (st.st_size < 0) return -1;
|
||||
*out_size = (size_t)st.st_size;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void descriptor_to_json(cJSON* obj, const blossom_blob_descriptor_t* d) {
|
||||
if (!obj || !d) return;
|
||||
cJSON_AddStringToObject(obj, "sha256", d->sha256);
|
||||
cJSON_AddStringToObject(obj, "url", d->url);
|
||||
cJSON_AddNumberToObject(obj, "size", d->size);
|
||||
cJSON_AddStringToObject(obj, "content_type", d->content_type);
|
||||
cJSON_AddNumberToObject(obj, "created", d->created);
|
||||
}
|
||||
|
||||
char* execute_blossom_upload(tools_context_t* ctx, const char* args_json) {
|
||||
if (!ctx || !ctx->cfg) return json_error_local("tool context unavailable");
|
||||
cJSON* args = parse_args_local(args_json);
|
||||
if (!args) return json_error_local("invalid arguments JSON");
|
||||
|
||||
cJSON* server = cJSON_GetObjectItemCaseSensitive(args, "server");
|
||||
cJSON* file_path = cJSON_GetObjectItemCaseSensitive(args, "file_path");
|
||||
cJSON* content_type = cJSON_GetObjectItemCaseSensitive(args, "content_type");
|
||||
|
||||
if (!server || !cJSON_IsString(server) || !server->valuestring || server->valuestring[0] == '\0' ||
|
||||
!file_path || !cJSON_IsString(file_path) || !file_path->valuestring || file_path->valuestring[0] == '\0') {
|
||||
cJSON_Delete(args);
|
||||
return json_error_local("blossom_upload requires server and file_path");
|
||||
}
|
||||
|
||||
if (!is_https_server_local(server->valuestring)) {
|
||||
cJSON_Delete(args);
|
||||
return json_error_local("blossom_upload requires an https:// server URL");
|
||||
}
|
||||
|
||||
char full_path[PATH_MAX];
|
||||
if (build_tool_path_local(ctx, file_path->valuestring, full_path, sizeof(full_path)) != 0) {
|
||||
cJSON_Delete(args);
|
||||
return json_error_local("blossom_upload file_path is not allowed");
|
||||
}
|
||||
|
||||
size_t input_size = 0;
|
||||
if (file_size_local(full_path, &input_size) != 0) {
|
||||
cJSON_Delete(args);
|
||||
return json_error_local("blossom_upload could not stat file_path");
|
||||
}
|
||||
|
||||
int upload_max = ctx->cfg->tools.blossom_max_upload_bytes > 0 ? ctx->cfg->tools.blossom_max_upload_bytes : (16 * 1024 * 1024);
|
||||
if (input_size > (size_t)upload_max) {
|
||||
cJSON_Delete(args);
|
||||
return json_error_local("blossom_upload file exceeds configured max upload size");
|
||||
}
|
||||
|
||||
blossom_blob_descriptor_t d;
|
||||
int rc = blossom_upload_file(server->valuestring,
|
||||
full_path,
|
||||
(content_type && cJSON_IsString(content_type) && content_type->valuestring) ? content_type->valuestring : NULL,
|
||||
ctx->cfg->keys.private_key,
|
||||
30,
|
||||
&d);
|
||||
cJSON_Delete(args);
|
||||
if (rc != NOSTR_SUCCESS) return json_error_local("blossom_upload failed");
|
||||
|
||||
cJSON* out = cJSON_CreateObject();
|
||||
if (!out) return NULL;
|
||||
cJSON_AddBoolToObject(out, "success", 1);
|
||||
cJSON_AddStringToObject(out, "server", server->valuestring);
|
||||
descriptor_to_json(out, &d);
|
||||
char* json = cJSON_PrintUnformatted(out);
|
||||
cJSON_Delete(out);
|
||||
return json;
|
||||
}
|
||||
|
||||
char* execute_blossom_download(tools_context_t* ctx, const char* args_json) {
|
||||
if (!ctx || !ctx->cfg) return json_error_local("tool context unavailable");
|
||||
cJSON* args = parse_args_local(args_json);
|
||||
if (!args) return json_error_local("invalid arguments JSON");
|
||||
|
||||
cJSON* server = cJSON_GetObjectItemCaseSensitive(args, "server");
|
||||
cJSON* sha = cJSON_GetObjectItemCaseSensitive(args, "sha256");
|
||||
cJSON* output_path = cJSON_GetObjectItemCaseSensitive(args, "output_path");
|
||||
|
||||
if (!server || !cJSON_IsString(server) || !server->valuestring || server->valuestring[0] == '\0' ||
|
||||
!sha || !cJSON_IsString(sha) || !sha->valuestring ||
|
||||
!output_path || !cJSON_IsString(output_path) || !output_path->valuestring) {
|
||||
cJSON_Delete(args);
|
||||
return json_error_local("blossom_download requires server, sha256, and output_path");
|
||||
}
|
||||
|
||||
if (!is_https_server_local(server->valuestring)) {
|
||||
cJSON_Delete(args);
|
||||
return json_error_local("blossom_download requires an https:// server URL");
|
||||
}
|
||||
|
||||
char full_path[PATH_MAX];
|
||||
if (build_tool_path_local(ctx, output_path->valuestring, full_path, sizeof(full_path)) != 0) {
|
||||
cJSON_Delete(args);
|
||||
return json_error_local("blossom_download output_path is not allowed");
|
||||
}
|
||||
|
||||
cJSON* overwrite = cJSON_GetObjectItemCaseSensitive(args, "overwrite");
|
||||
int allow_overwrite = (overwrite && cJSON_IsBool(overwrite) && cJSON_IsTrue(overwrite)) ? 1 : 0;
|
||||
if (!allow_overwrite && file_exists_local(full_path)) {
|
||||
cJSON_Delete(args);
|
||||
return json_error_local("blossom_download output_path exists (set overwrite=true to replace)");
|
||||
}
|
||||
|
||||
int download_max = ctx->cfg->tools.blossom_max_download_bytes > 0 ? ctx->cfg->tools.blossom_max_download_bytes : (16 * 1024 * 1024);
|
||||
blossom_blob_descriptor_t d;
|
||||
int rc = blossom_download_to_file(server->valuestring, sha->valuestring, full_path, 30, (size_t)download_max, &d);
|
||||
cJSON_Delete(args);
|
||||
if (rc != NOSTR_SUCCESS) return json_error_local("blossom_download failed");
|
||||
|
||||
cJSON* out = cJSON_CreateObject();
|
||||
if (!out) return NULL;
|
||||
cJSON_AddBoolToObject(out, "success", 1);
|
||||
cJSON_AddStringToObject(out, "output_path", full_path);
|
||||
descriptor_to_json(out, &d);
|
||||
char* json = cJSON_PrintUnformatted(out);
|
||||
cJSON_Delete(out);
|
||||
return json;
|
||||
}
|
||||
|
||||
char* execute_blossom_head(tools_context_t* ctx, const char* args_json) {
|
||||
if (!ctx || !ctx->cfg) return json_error_local("tool context unavailable");
|
||||
cJSON* args = parse_args_local(args_json);
|
||||
if (!args) return json_error_local("invalid arguments JSON");
|
||||
|
||||
cJSON* server = cJSON_GetObjectItemCaseSensitive(args, "server");
|
||||
cJSON* sha = cJSON_GetObjectItemCaseSensitive(args, "sha256");
|
||||
if (!server || !cJSON_IsString(server) || !server->valuestring || !sha || !cJSON_IsString(sha) || !sha->valuestring) {
|
||||
cJSON_Delete(args);
|
||||
return json_error_local("blossom_head requires server and sha256");
|
||||
}
|
||||
|
||||
if (!is_https_server_local(server->valuestring)) {
|
||||
cJSON_Delete(args);
|
||||
return json_error_local("blossom_head requires an https:// server URL");
|
||||
}
|
||||
|
||||
blossom_blob_descriptor_t d;
|
||||
int rc = blossom_head(server->valuestring, sha->valuestring, 15, &d);
|
||||
cJSON_Delete(args);
|
||||
if (rc != NOSTR_SUCCESS) return json_error_local("blossom_head failed");
|
||||
|
||||
cJSON* out = cJSON_CreateObject();
|
||||
if (!out) return NULL;
|
||||
cJSON_AddBoolToObject(out, "success", 1);
|
||||
cJSON_AddBoolToObject(out, "exists", 1);
|
||||
descriptor_to_json(out, &d);
|
||||
char* json = cJSON_PrintUnformatted(out);
|
||||
cJSON_Delete(out);
|
||||
return json;
|
||||
}
|
||||
|
||||
char* execute_blossom_delete(tools_context_t* ctx, const char* args_json) {
|
||||
if (!ctx || !ctx->cfg) return json_error_local("tool context unavailable");
|
||||
cJSON* args = parse_args_local(args_json);
|
||||
if (!args) return json_error_local("invalid arguments JSON");
|
||||
|
||||
cJSON* server = cJSON_GetObjectItemCaseSensitive(args, "server");
|
||||
cJSON* sha = cJSON_GetObjectItemCaseSensitive(args, "sha256");
|
||||
if (!server || !cJSON_IsString(server) || !server->valuestring || !sha || !cJSON_IsString(sha) || !sha->valuestring) {
|
||||
cJSON_Delete(args);
|
||||
return json_error_local("blossom_delete requires server and sha256");
|
||||
}
|
||||
|
||||
if (!is_https_server_local(server->valuestring)) {
|
||||
cJSON_Delete(args);
|
||||
return json_error_local("blossom_delete requires an https:// server URL");
|
||||
}
|
||||
|
||||
int rc = blossom_delete(server->valuestring, sha->valuestring, ctx->cfg->keys.private_key, 15);
|
||||
cJSON_Delete(args);
|
||||
if (rc != NOSTR_SUCCESS) return json_error_local("blossom_delete failed");
|
||||
|
||||
cJSON* out = cJSON_CreateObject();
|
||||
if (!out) return NULL;
|
||||
cJSON_AddBoolToObject(out, "success", 1);
|
||||
cJSON_AddBoolToObject(out, "deleted", 1);
|
||||
cJSON_AddStringToObject(out, "sha256", sha->valuestring);
|
||||
char* json = cJSON_PrintUnformatted(out);
|
||||
cJSON_Delete(out);
|
||||
return json;
|
||||
}
|
||||
|
||||
char* execute_blossom_list(tools_context_t* ctx, const char* args_json) {
|
||||
if (!ctx || !ctx->cfg) return json_error_local("tool context unavailable");
|
||||
cJSON* args = parse_args_local(args_json);
|
||||
if (!args) return json_error_local("invalid arguments JSON");
|
||||
|
||||
cJSON* server = cJSON_GetObjectItemCaseSensitive(args, "server");
|
||||
cJSON* pubkey = cJSON_GetObjectItemCaseSensitive(args, "pubkey");
|
||||
if (!server || !cJSON_IsString(server) || !server->valuestring) {
|
||||
cJSON_Delete(args);
|
||||
return json_error_local("blossom_list requires server");
|
||||
}
|
||||
|
||||
if (!is_https_server_local(server->valuestring)) {
|
||||
cJSON_Delete(args);
|
||||
return json_error_local("blossom_list requires an https:// server URL");
|
||||
}
|
||||
|
||||
const char* pk = (pubkey && cJSON_IsString(pubkey) && pubkey->valuestring && pubkey->valuestring[0] != '\0')
|
||||
? pubkey->valuestring
|
||||
: ctx->cfg->keys.public_key_hex;
|
||||
|
||||
blossom_blob_descriptor_t* items = NULL;
|
||||
int count = 0;
|
||||
int rc = blossom_list(server->valuestring, pk, 20, &items, &count);
|
||||
cJSON_Delete(args);
|
||||
if (rc != NOSTR_SUCCESS) return json_error_local("blossom_list failed");
|
||||
|
||||
cJSON* out = cJSON_CreateObject();
|
||||
cJSON* arr = cJSON_CreateArray();
|
||||
if (!out || !arr) {
|
||||
free(items);
|
||||
cJSON_Delete(out);
|
||||
cJSON_Delete(arr);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
cJSON_AddBoolToObject(out, "success", 1);
|
||||
cJSON_AddStringToObject(out, "pubkey", pk);
|
||||
cJSON_AddNumberToObject(out, "count", count);
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
cJSON* it = cJSON_CreateObject();
|
||||
if (!it) continue;
|
||||
descriptor_to_json(it, &items[i]);
|
||||
cJSON_AddItemToArray(arr, it);
|
||||
}
|
||||
free(items);
|
||||
|
||||
cJSON_AddItemToObject(out, "blobs", arr);
|
||||
char* json = cJSON_PrintUnformatted(out);
|
||||
cJSON_Delete(out);
|
||||
return json;
|
||||
}
|
||||
+48
-129
@@ -2,7 +2,6 @@
|
||||
|
||||
#include "tools_internal.h"
|
||||
|
||||
#include <curl/curl.h>
|
||||
#include <limits.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
@@ -13,13 +12,7 @@
|
||||
|
||||
#include "cjson/cJSON.h"
|
||||
|
||||
typedef struct {
|
||||
char* data;
|
||||
size_t len;
|
||||
size_t cap;
|
||||
size_t max_bytes;
|
||||
int truncated;
|
||||
} local_http_fetch_buffer_t;
|
||||
#include "../../nostr_core_lib/nostr_core/nostr_http.h"
|
||||
|
||||
static char* json_error_local(const char* msg) {
|
||||
cJSON* root = cJSON_CreateObject();
|
||||
@@ -102,63 +95,6 @@ static char* shell_quote_single_local(const char* in) {
|
||||
return out;
|
||||
}
|
||||
|
||||
static size_t local_http_fetch_write_cb_local(void* contents, size_t size, size_t nmemb, void* userp) {
|
||||
local_http_fetch_buffer_t* rb = (local_http_fetch_buffer_t*)userp;
|
||||
size_t total = size * nmemb;
|
||||
if (!rb || total == 0) return total;
|
||||
|
||||
if (rb->len >= rb->max_bytes) {
|
||||
rb->truncated = 1;
|
||||
return total;
|
||||
}
|
||||
|
||||
size_t allowed = rb->max_bytes - rb->len;
|
||||
size_t to_copy = total <= allowed ? total : allowed;
|
||||
|
||||
if (rb->len + to_copy + 1U > rb->cap) {
|
||||
size_t new_cap = rb->cap == 0 ? 1024U : rb->cap;
|
||||
while (new_cap < rb->len + to_copy + 1U) {
|
||||
new_cap *= 2U;
|
||||
}
|
||||
char* bigger = (char*)realloc(rb->data, new_cap);
|
||||
if (!bigger) return 0;
|
||||
rb->data = bigger;
|
||||
rb->cap = new_cap;
|
||||
}
|
||||
|
||||
memcpy(rb->data + rb->len, contents, to_copy);
|
||||
rb->len += to_copy;
|
||||
rb->data[rb->len] = '\0';
|
||||
|
||||
if (to_copy < total) {
|
||||
rb->truncated = 1;
|
||||
}
|
||||
|
||||
return total;
|
||||
}
|
||||
|
||||
static const char* detect_ca_bundle_path_for_tools_local(void) {
|
||||
const char* env = getenv("SSL_CERT_FILE");
|
||||
if (env && env[0] != '\0' && access(env, R_OK) == 0) {
|
||||
return env;
|
||||
}
|
||||
|
||||
static const char* candidates[] = {
|
||||
"/etc/ssl/certs/ca-certificates.crt",
|
||||
"/etc/ssl/cert.pem",
|
||||
"/etc/pki/tls/certs/ca-bundle.crt",
|
||||
"/etc/ssl/ca-bundle.pem"
|
||||
};
|
||||
|
||||
for (size_t i = 0; i < sizeof(candidates) / sizeof(candidates[0]); i++) {
|
||||
if (access(candidates[i], R_OK) == 0) {
|
||||
return candidates[i];
|
||||
}
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
char* execute_local_http_fetch(tools_context_t* ctx, const char* args_json) {
|
||||
if (!ctx || !ctx->cfg) return json_error_local("tool context unavailable");
|
||||
|
||||
@@ -212,104 +148,87 @@ char* execute_local_http_fetch(tools_context_t* ctx, const char* args_json) {
|
||||
int max_bytes = (maxb && cJSON_IsNumber(maxb)) ? (int)maxb->valuedouble : hard_max;
|
||||
if (max_bytes <= 0 || max_bytes > hard_max) max_bytes = hard_max;
|
||||
|
||||
CURL* curl = curl_easy_init();
|
||||
if (!curl) {
|
||||
cJSON_Delete(args);
|
||||
return json_error_local("local_http_fetch failed to initialize curl");
|
||||
const char* default_header = "Accept: */*";
|
||||
int hdr_count = 1;
|
||||
if (headers && cJSON_IsArray(headers)) {
|
||||
hdr_count += cJSON_GetArraySize(headers);
|
||||
}
|
||||
|
||||
local_http_fetch_buffer_t rb;
|
||||
memset(&rb, 0, sizeof(rb));
|
||||
rb.max_bytes = (size_t)max_bytes;
|
||||
|
||||
struct curl_slist* req_headers = NULL;
|
||||
req_headers = curl_slist_append(req_headers, "Accept: */*");
|
||||
|
||||
const char** req_headers = (const char**)calloc((size_t)hdr_count + 1U, sizeof(const char*));
|
||||
if (!req_headers) {
|
||||
cJSON_Delete(args);
|
||||
return json_error_local("local_http_fetch allocation failure");
|
||||
}
|
||||
int hi = 0;
|
||||
req_headers[hi++] = default_header;
|
||||
if (headers && cJSON_IsArray(headers)) {
|
||||
int n = cJSON_GetArraySize(headers);
|
||||
for (int i = 0; i < n; i++) {
|
||||
cJSON* h = cJSON_GetArrayItem(headers, i);
|
||||
if (h && cJSON_IsString(h) && h->valuestring && h->valuestring[0] != '\0') {
|
||||
req_headers = curl_slist_append(req_headers, h->valuestring);
|
||||
req_headers[hi++] = h->valuestring;
|
||||
}
|
||||
}
|
||||
}
|
||||
req_headers[hi] = NULL;
|
||||
|
||||
curl_easy_setopt(curl, CURLOPT_URL, url->valuestring);
|
||||
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
|
||||
curl_easy_setopt(curl, CURLOPT_TIMEOUT, (long)timeout_seconds);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, local_http_fetch_write_cb_local);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &rb);
|
||||
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, req_headers);
|
||||
curl_easy_setopt(curl, CURLOPT_USERAGENT, "didactyl/local_http_fetch");
|
||||
|
||||
const char* ca_bundle = detect_ca_bundle_path_for_tools_local();
|
||||
if (ca_bundle) {
|
||||
curl_easy_setopt(curl, CURLOPT_CAINFO, ca_bundle);
|
||||
nostr_http_request_t req;
|
||||
memset(&req, 0, sizeof(req));
|
||||
req.method = method_str;
|
||||
req.url = url->valuestring;
|
||||
req.headers = req_headers;
|
||||
req.timeout_seconds = timeout_seconds;
|
||||
req.max_response_bytes = (size_t)max_bytes;
|
||||
req.follow_redirects = 1;
|
||||
req.max_redirects = 3;
|
||||
req.user_agent = "didactyl/local_http_fetch";
|
||||
if (body && cJSON_IsString(body) && body->valuestring) {
|
||||
req.body = (const unsigned char*)body->valuestring;
|
||||
req.body_len = strlen(body->valuestring);
|
||||
}
|
||||
|
||||
if (strcasecmp(method_str, "GET") == 0) {
|
||||
curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L);
|
||||
} else if (strcasecmp(method_str, "POST") == 0) {
|
||||
curl_easy_setopt(curl, CURLOPT_POST, 1L);
|
||||
if (body && cJSON_IsString(body) && body->valuestring) {
|
||||
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body->valuestring);
|
||||
curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, (long)strlen(body->valuestring));
|
||||
}
|
||||
} else {
|
||||
curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, method_str);
|
||||
if (body && cJSON_IsString(body) && body->valuestring) {
|
||||
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body->valuestring);
|
||||
curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, (long)strlen(body->valuestring));
|
||||
}
|
||||
}
|
||||
|
||||
CURLcode res = curl_easy_perform(curl);
|
||||
long status_code = 0;
|
||||
char* content_type = NULL;
|
||||
char* content_type_copy = NULL;
|
||||
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &status_code);
|
||||
curl_easy_getinfo(curl, CURLINFO_CONTENT_TYPE, &content_type);
|
||||
if (content_type && content_type[0] != '\0') {
|
||||
content_type_copy = strdup(content_type);
|
||||
}
|
||||
|
||||
curl_slist_free_all(req_headers);
|
||||
curl_easy_cleanup(curl);
|
||||
nostr_http_response_t resp;
|
||||
int rc = nostr_http_request(&req, &resp);
|
||||
|
||||
cJSON* out = cJSON_CreateObject();
|
||||
if (!out) {
|
||||
free(rb.data);
|
||||
free(req_headers);
|
||||
cJSON_Delete(args);
|
||||
if (rc == NOSTR_SUCCESS) {
|
||||
nostr_http_response_free(&resp);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int http_ok = (status_code >= 200 && status_code < 300) ? 1 : 0;
|
||||
int success = (res == CURLE_OK && http_ok) ? 1 : 0;
|
||||
int http_ok = (rc == NOSTR_SUCCESS && resp.status_code >= 200 && resp.status_code < 300) ? 1 : 0;
|
||||
int success = http_ok ? 1 : 0;
|
||||
|
||||
cJSON_AddBoolToObject(out, "success", success);
|
||||
cJSON_AddStringToObject(out, "url", url->valuestring);
|
||||
cJSON_AddStringToObject(out, "method", method_str);
|
||||
cJSON_AddNumberToObject(out, "status_code", status_code);
|
||||
cJSON_AddNumberToObject(out, "status_code", rc == NOSTR_SUCCESS ? resp.status_code : 0);
|
||||
cJSON_AddBoolToObject(out, "http_ok", http_ok);
|
||||
cJSON_AddBoolToObject(out, "truncated", rb.truncated ? 1 : 0);
|
||||
cJSON_AddNumberToObject(out, "bytes_received", (double)rb.len);
|
||||
cJSON_AddBoolToObject(out, "truncated", (rc == NOSTR_SUCCESS && resp.truncated) ? 1 : 0);
|
||||
cJSON_AddNumberToObject(out, "bytes_received", rc == NOSTR_SUCCESS ? (double)resp.body_len : 0.0);
|
||||
|
||||
if (content_type_copy && content_type_copy[0] != '\0') {
|
||||
cJSON_AddStringToObject(out, "content_type", content_type_copy);
|
||||
if (rc == NOSTR_SUCCESS && resp.content_type && resp.content_type[0] != '\0') {
|
||||
cJSON_AddStringToObject(out, "content_type", resp.content_type);
|
||||
}
|
||||
|
||||
if (res != CURLE_OK) {
|
||||
cJSON_AddStringToObject(out, "curl_error", curl_easy_strerror(res));
|
||||
if (rc != NOSTR_SUCCESS) {
|
||||
cJSON_AddStringToObject(out, "curl_error", "request failed");
|
||||
}
|
||||
|
||||
cJSON_AddStringToObject(out, "body", rb.data ? rb.data : "");
|
||||
cJSON_AddStringToObject(out, "body", (rc == NOSTR_SUCCESS && resp.body) ? resp.body : "");
|
||||
|
||||
free(rb.data);
|
||||
if (rc == NOSTR_SUCCESS) {
|
||||
nostr_http_response_free(&resp);
|
||||
}
|
||||
|
||||
char* json = cJSON_PrintUnformatted(out);
|
||||
cJSON_Delete(out);
|
||||
cJSON_Delete(args);
|
||||
free(content_type_copy);
|
||||
free(req_headers);
|
||||
return json;
|
||||
}
|
||||
|
||||
|
||||
@@ -274,6 +274,21 @@ char* tools_execute_legacy(tools_context_t* ctx, const char* tool_name, const ch
|
||||
if (strcmp(tool_name, "cashu_wallet_mints_set") == 0) {
|
||||
return execute_cashu_wallet_mints_set(ctx, args_json);
|
||||
}
|
||||
if (strcmp(tool_name, "blossom_upload") == 0) {
|
||||
return execute_blossom_upload(ctx, args_json);
|
||||
}
|
||||
if (strcmp(tool_name, "blossom_download") == 0) {
|
||||
return execute_blossom_download(ctx, args_json);
|
||||
}
|
||||
if (strcmp(tool_name, "blossom_head") == 0) {
|
||||
return execute_blossom_head(ctx, args_json);
|
||||
}
|
||||
if (strcmp(tool_name, "blossom_delete") == 0) {
|
||||
return execute_blossom_delete(ctx, args_json);
|
||||
}
|
||||
if (strcmp(tool_name, "blossom_list") == 0) {
|
||||
return execute_blossom_list(ctx, args_json);
|
||||
}
|
||||
|
||||
return json_error("unknown tool");
|
||||
}
|
||||
|
||||
@@ -84,6 +84,12 @@ char* execute_cashu_wallet_send_token(tools_context_t* ctx, const char* args_jso
|
||||
char* execute_cashu_wallet_mints_get(tools_context_t* ctx, const char* args_json);
|
||||
char* execute_cashu_wallet_mints_set(tools_context_t* ctx, const char* args_json);
|
||||
|
||||
char* execute_blossom_upload(tools_context_t* ctx, const char* args_json);
|
||||
char* execute_blossom_download(tools_context_t* ctx, const char* args_json);
|
||||
char* execute_blossom_head(tools_context_t* ctx, const char* args_json);
|
||||
char* execute_blossom_delete(tools_context_t* ctx, const char* args_json);
|
||||
char* execute_blossom_list(tools_context_t* ctx, const char* args_json);
|
||||
|
||||
int memory_init(tools_context_t* ctx);
|
||||
void memory_cleanup(void);
|
||||
|
||||
|
||||
@@ -1908,6 +1908,104 @@ char* tools_build_openai_schema_json_legacy(const tools_context_t* ctx) {
|
||||
cJSON_AddItemToObject(t62, "function", t62_fn);
|
||||
cJSON_AddItemToArray(tools, t62);
|
||||
|
||||
cJSON* t63 = cJSON_CreateObject();
|
||||
cJSON* t63_fn = cJSON_CreateObject();
|
||||
cJSON* t63_params = cJSON_CreateObject();
|
||||
cJSON* t63_props = cJSON_CreateObject();
|
||||
cJSON* t63_required = cJSON_CreateArray();
|
||||
cJSON_AddStringToObject(t63, "type", "function");
|
||||
cJSON_AddStringToObject(t63_fn, "name", "blossom_upload");
|
||||
cJSON_AddStringToObject(t63_fn, "description", "Upload a local file to a Blossom server");
|
||||
cJSON_AddStringToObject(t63_params, "type", "object");
|
||||
cJSON_AddItemToObject(t63_params, "properties", t63_props);
|
||||
cJSON_AddItemToObject(t63_params, "required", t63_required);
|
||||
cJSON* p63_server = cJSON_CreateObject(); cJSON_AddStringToObject(p63_server, "type", "string"); cJSON_AddItemToObject(t63_props, "server", p63_server);
|
||||
cJSON* p63_file = cJSON_CreateObject(); cJSON_AddStringToObject(p63_file, "type", "string"); cJSON_AddItemToObject(t63_props, "file_path", p63_file);
|
||||
cJSON* p63_ct = cJSON_CreateObject(); cJSON_AddStringToObject(p63_ct, "type", "string"); cJSON_AddItemToObject(t63_props, "content_type", p63_ct);
|
||||
cJSON_AddItemToArray(t63_required, cJSON_CreateString("server"));
|
||||
cJSON_AddItemToArray(t63_required, cJSON_CreateString("file_path"));
|
||||
cJSON_AddItemToObject(t63_fn, "parameters", t63_params);
|
||||
cJSON_AddItemToObject(t63, "function", t63_fn);
|
||||
cJSON_AddItemToArray(tools, t63);
|
||||
|
||||
cJSON* t64 = cJSON_CreateObject();
|
||||
cJSON* t64_fn = cJSON_CreateObject();
|
||||
cJSON* t64_params = cJSON_CreateObject();
|
||||
cJSON* t64_props = cJSON_CreateObject();
|
||||
cJSON* t64_required = cJSON_CreateArray();
|
||||
cJSON_AddStringToObject(t64, "type", "function");
|
||||
cJSON_AddStringToObject(t64_fn, "name", "blossom_download");
|
||||
cJSON_AddStringToObject(t64_fn, "description", "Download a blob from Blossom to a local file");
|
||||
cJSON_AddStringToObject(t64_params, "type", "object");
|
||||
cJSON_AddItemToObject(t64_params, "properties", t64_props);
|
||||
cJSON_AddItemToObject(t64_params, "required", t64_required);
|
||||
cJSON* p64_server = cJSON_CreateObject(); cJSON_AddStringToObject(p64_server, "type", "string"); cJSON_AddItemToObject(t64_props, "server", p64_server);
|
||||
cJSON* p64_sha = cJSON_CreateObject(); cJSON_AddStringToObject(p64_sha, "type", "string"); cJSON_AddItemToObject(t64_props, "sha256", p64_sha);
|
||||
cJSON* p64_out = cJSON_CreateObject(); cJSON_AddStringToObject(p64_out, "type", "string"); cJSON_AddItemToObject(t64_props, "output_path", p64_out);
|
||||
cJSON* p64_overwrite = cJSON_CreateObject(); cJSON_AddStringToObject(p64_overwrite, "type", "boolean"); cJSON_AddItemToObject(t64_props, "overwrite", p64_overwrite);
|
||||
cJSON_AddItemToArray(t64_required, cJSON_CreateString("server"));
|
||||
cJSON_AddItemToArray(t64_required, cJSON_CreateString("sha256"));
|
||||
cJSON_AddItemToArray(t64_required, cJSON_CreateString("output_path"));
|
||||
cJSON_AddItemToObject(t64_fn, "parameters", t64_params);
|
||||
cJSON_AddItemToObject(t64, "function", t64_fn);
|
||||
cJSON_AddItemToArray(tools, t64);
|
||||
|
||||
cJSON* t65 = cJSON_CreateObject();
|
||||
cJSON* t65_fn = cJSON_CreateObject();
|
||||
cJSON* t65_params = cJSON_CreateObject();
|
||||
cJSON* t65_props = cJSON_CreateObject();
|
||||
cJSON* t65_required = cJSON_CreateArray();
|
||||
cJSON_AddStringToObject(t65, "type", "function");
|
||||
cJSON_AddStringToObject(t65_fn, "name", "blossom_head");
|
||||
cJSON_AddStringToObject(t65_fn, "description", "Fetch blob metadata from Blossom");
|
||||
cJSON_AddStringToObject(t65_params, "type", "object");
|
||||
cJSON_AddItemToObject(t65_params, "properties", t65_props);
|
||||
cJSON_AddItemToObject(t65_params, "required", t65_required);
|
||||
cJSON* p65_server = cJSON_CreateObject(); cJSON_AddStringToObject(p65_server, "type", "string"); cJSON_AddItemToObject(t65_props, "server", p65_server);
|
||||
cJSON* p65_sha = cJSON_CreateObject(); cJSON_AddStringToObject(p65_sha, "type", "string"); cJSON_AddItemToObject(t65_props, "sha256", p65_sha);
|
||||
cJSON_AddItemToArray(t65_required, cJSON_CreateString("server"));
|
||||
cJSON_AddItemToArray(t65_required, cJSON_CreateString("sha256"));
|
||||
cJSON_AddItemToObject(t65_fn, "parameters", t65_params);
|
||||
cJSON_AddItemToObject(t65, "function", t65_fn);
|
||||
cJSON_AddItemToArray(tools, t65);
|
||||
|
||||
cJSON* t66 = cJSON_CreateObject();
|
||||
cJSON* t66_fn = cJSON_CreateObject();
|
||||
cJSON* t66_params = cJSON_CreateObject();
|
||||
cJSON* t66_props = cJSON_CreateObject();
|
||||
cJSON* t66_required = cJSON_CreateArray();
|
||||
cJSON_AddStringToObject(t66, "type", "function");
|
||||
cJSON_AddStringToObject(t66_fn, "name", "blossom_delete");
|
||||
cJSON_AddStringToObject(t66_fn, "description", "Delete a blob from Blossom");
|
||||
cJSON_AddStringToObject(t66_params, "type", "object");
|
||||
cJSON_AddItemToObject(t66_params, "properties", t66_props);
|
||||
cJSON_AddItemToObject(t66_params, "required", t66_required);
|
||||
cJSON* p66_server = cJSON_CreateObject(); cJSON_AddStringToObject(p66_server, "type", "string"); cJSON_AddItemToObject(t66_props, "server", p66_server);
|
||||
cJSON* p66_sha = cJSON_CreateObject(); cJSON_AddStringToObject(p66_sha, "type", "string"); cJSON_AddItemToObject(t66_props, "sha256", p66_sha);
|
||||
cJSON_AddItemToArray(t66_required, cJSON_CreateString("server"));
|
||||
cJSON_AddItemToArray(t66_required, cJSON_CreateString("sha256"));
|
||||
cJSON_AddItemToObject(t66_fn, "parameters", t66_params);
|
||||
cJSON_AddItemToObject(t66, "function", t66_fn);
|
||||
cJSON_AddItemToArray(tools, t66);
|
||||
|
||||
cJSON* t67 = cJSON_CreateObject();
|
||||
cJSON* t67_fn = cJSON_CreateObject();
|
||||
cJSON* t67_params = cJSON_CreateObject();
|
||||
cJSON* t67_props = cJSON_CreateObject();
|
||||
cJSON* t67_required = cJSON_CreateArray();
|
||||
cJSON_AddStringToObject(t67, "type", "function");
|
||||
cJSON_AddStringToObject(t67_fn, "name", "blossom_list");
|
||||
cJSON_AddStringToObject(t67_fn, "description", "List blobs for a pubkey on Blossom");
|
||||
cJSON_AddStringToObject(t67_params, "type", "object");
|
||||
cJSON_AddItemToObject(t67_params, "properties", t67_props);
|
||||
cJSON_AddItemToObject(t67_params, "required", t67_required);
|
||||
cJSON* p67_server = cJSON_CreateObject(); cJSON_AddStringToObject(p67_server, "type", "string"); cJSON_AddItemToObject(t67_props, "server", p67_server);
|
||||
cJSON* p67_pubkey = cJSON_CreateObject(); cJSON_AddStringToObject(p67_pubkey, "type", "string"); cJSON_AddItemToObject(t67_props, "pubkey", p67_pubkey);
|
||||
cJSON_AddItemToArray(t67_required, cJSON_CreateString("server"));
|
||||
cJSON_AddItemToObject(t67_fn, "parameters", t67_params);
|
||||
cJSON_AddItemToObject(t67, "function", t67_fn);
|
||||
cJSON_AddItemToArray(tools, t67);
|
||||
|
||||
char* out = cJSON_PrintUnformatted(tools);
|
||||
cJSON_Delete(tools);
|
||||
return out;
|
||||
|
||||
Executable
BIN
Binary file not shown.
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* Didactyl Blossom tool argument validation tests.
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "../src/config.h"
|
||||
#include "../src/tools/tools.h"
|
||||
#include "../src/tools/tools_internal.h"
|
||||
|
||||
static int tests_run = 0;
|
||||
static int tests_passed = 0;
|
||||
|
||||
#define TEST_ASSERT(cond, msg) do { \
|
||||
tests_run++; \
|
||||
if (cond) { tests_passed++; printf("✅ %s\n", msg); } \
|
||||
else { printf("❌ %s\n", msg); } \
|
||||
} while (0)
|
||||
|
||||
static int json_contains_error(const char* json, const char* needle) {
|
||||
return json && strstr(json, "\"success\":false") && strstr(json, needle);
|
||||
}
|
||||
|
||||
static int write_file_text(const char* path, const char* text) {
|
||||
FILE* fp = fopen(path, "wb");
|
||||
if (!fp) return -1;
|
||||
size_t n = fwrite(text, 1, strlen(text), fp);
|
||||
fclose(fp);
|
||||
return (n == strlen(text)) ? 0 : -1;
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
printf("Didactyl Blossom tool validation tests\n");
|
||||
printf("======================================\n");
|
||||
|
||||
didactyl_config_t cfg;
|
||||
memset(&cfg, 0, sizeof(cfg));
|
||||
snprintf(cfg.tools.shell.working_directory, sizeof(cfg.tools.shell.working_directory), "%s", ".");
|
||||
cfg.tools.blossom_max_upload_bytes = 16;
|
||||
cfg.tools.blossom_max_download_bytes = 1024;
|
||||
|
||||
tools_context_t ctx;
|
||||
memset(&ctx, 0, sizeof(ctx));
|
||||
ctx.cfg = &cfg;
|
||||
|
||||
char* out = execute_blossom_upload(&ctx, "{\"server\":\"http://example.com\",\"file_path\":\"README.md\"}");
|
||||
TEST_ASSERT(json_contains_error(out, "https://"), "blossom_upload rejects non-https server");
|
||||
free(out);
|
||||
|
||||
const char* big_file = "tests/.tmp_blossom_big.txt";
|
||||
if (write_file_text(big_file, "this payload is intentionally larger than 16 bytes") != 0) {
|
||||
printf("❌ setup failed to create temp file\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
out = execute_blossom_upload(&ctx, "{\"server\":\"https://example.com\",\"file_path\":\"tests/.tmp_blossom_big.txt\"}");
|
||||
TEST_ASSERT(json_contains_error(out, "max upload size"), "blossom_upload enforces configured upload max");
|
||||
free(out);
|
||||
|
||||
const char* existing = "tests/.tmp_blossom_existing.txt";
|
||||
if (write_file_text(existing, "already here") != 0) {
|
||||
printf("❌ setup failed to create existing output file\n");
|
||||
remove(big_file);
|
||||
return 1;
|
||||
}
|
||||
|
||||
out = execute_blossom_download(&ctx,
|
||||
"{\"server\":\"https://example.com\",\"sha256\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"output_path\":\"tests/.tmp_blossom_existing.txt\"}");
|
||||
TEST_ASSERT(json_contains_error(out, "output_path exists"), "blossom_download requires overwrite=true when file exists");
|
||||
free(out);
|
||||
|
||||
remove(big_file);
|
||||
remove(existing);
|
||||
|
||||
printf("\nSummary: %d/%d passed\n", tests_passed, tests_run);
|
||||
return (tests_passed == tests_run) ? 0 : 1;
|
||||
}
|
||||
Reference in New Issue
Block a user