Compare commits

..
2 Commits
23 changed files with 1490 additions and 267 deletions
+1 -1
View File
@@ -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 \
+1
View File
@@ -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 \
+2 -2
View File
@@ -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.19Prevent 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
+2
View File
@@ -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,
+109
View File
@@ -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."
+10
View File
@@ -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 |
+668
View File
@@ -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
View File
@@ -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) {
+18
View File
@@ -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;
+2
View File
@@ -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 {
+37 -100
View File
@@ -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
View File
@@ -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
View File
@@ -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"
+62
View File
@@ -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;
+1
View File
@@ -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);
+2
View File
@@ -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;
+304
View File
@@ -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
View File
@@ -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;
}
+15
View File
@@ -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");
}
+6
View File
@@ -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);
+98
View File
@@ -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;
BIN
View File
Binary file not shown.
+79
View File
@@ -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;
}