21 KiB
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 APInostr_core_lib/nostr_core/nostr_http.c— single curl implementation
Proposed API
#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
- Binary-safe body —
bodyisunsigned char*with explicitbody_len, supporting both JSON text and raw file uploads - Method-agnostic — supports GET, POST, PUT, DELETE, HEAD, PATCH via string
- Response size cap —
max_response_bytesprevents OOM on large downloads - Header capture — optional
capture_headersfor HEAD requests (Blossom needs this) - CA bundle — single global setter replaces 5 separate detection functions
- Convenience wrappers —
nostr_http_get()andnostr_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:
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:
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.cto 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.hto includenostr_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 callbackstatic const char* detect_ca_bundle_path()— replaced bynostr_http_detect_ca_bundle()typedef struct { char* data; size_t len; } response_buffer_t;— replaced bynostr_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()— replacedstatic const char* detect_ca_bundle_path_for_tools_local()— replacedtypedef 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:
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 APInostr_core_lib/nostr_core/blossom_client.c— implementation usingnostr_http
Proposed API
#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()usesnostr_create_and_sign_event()(kind 24242) +base64_encode()- All HTTP calls go through
nostr_http_request()— no direct curl usage blossom_upload_file()usesnostr_sha256_file_stream()to hash before uploadblossom_download_to_file()verifies SHA-256 after download
Build changes
- Add
blossom_client.cto nostr_core_lib build - Update
nostr_core.hto includeblossom_client.h - Rebuild
libnostr_core_*.a
Phase 5: Blossom Tools in Didactyl
New file
src/tools/tool_blossom.c— thin tool wrappers callingblossom_*()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
tools_internal.h— addexecute_blossom_*()prototypestools_dispatch.c— addstrcmpbranchestools_schema.c— add OpenAI tool definitionsMakefile— add$(SRC_DIR)/tools/tool_blossom.cto SRCSdocs/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:
{
"success": true,
"error": "...optional on failure..."
}
blossom_upload
Input:
{
"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:
{
"success": true,
"server": "https://blossom.example",
"sha256": "<64-hex>",
"size": 12345,
"content_type": "image/png",
"url": "https://blossom.example/<sha256>"
}
blossom_download
Input:
{
"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:
{
"success": true,
"output_path": "downloads/file.bin",
"bytes_written": 12345,
"sha256": "<computed-64-hex>",
"verified": true,
"content_type": "application/octet-stream"
}
blossom_head
Input:
{
"type": "object",
"properties": {
"server": { "type": "string" },
"sha256": { "type": "string" },
"url": { "type": "string" }
}
}
Output:
{
"success": true,
"exists": true,
"sha256": "<64-hex>",
"size": 12345,
"content_type": "image/jpeg"
}
blossom_delete
Input:
{
"type": "object",
"properties": {
"server": { "type": "string" },
"sha256": { "type": "string" }
},
"required": ["sha256"]
}
Output:
{
"success": true,
"deleted": true,
"server": "https://blossom.example",
"sha256": "<64-hex>"
}
blossom_list
Input:
{
"type": "object",
"properties": {
"server": { "type": "string" },
"pubkey": { "type": "string", "description": "Hex pubkey; defaults to agent pubkey" }
}
}
Output:
{
"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
servermust behttps://unless explicit config allows insecure local testingsha256must match^[0-9a-fA-F]{64}$file_pathandoutput_pathmust be safe relative paths (reusetool_local.cpattern)- Enforce maximum upload/download size from config
- Refuse overwrite unless
overwrite=true - Normalize all errors into
{"success": false, "error": "..."} - Do not leak keys/secrets in returned payload
Execution Order
Step 1: nostr_http in nostr_core_lib
- Create
nostr_http.handnostr_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.cto usenostr_http - Refactor
nip005.cto usenostr_http - Refactor
nip011.cto usenostr_http - Remove direct
#include <curl/curl.h>from all three - Run existing tests to verify no regressions
Step 3: Migrate Didactyl consumers
- Refactor
llm.cto usenostr_http - Refactor
tool_local.cto usenostr_http - Update
cashu_wallet.cCA 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.handblossom_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
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
nostr_http— GET/POST/PUT/DELETE/HEAD, timeouts, max_response_bytes, CA bundleblossom_client— auth event creation, upload/download/head/delete/list- 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_fetchtool works correctly after tool_local.c migration
Risks and Mitigations
-
nostr_core_lib API change breaks Didactyl build
- Mitigation: version-pin nostr_core_lib; test both projects together before release
-
Subtle curl behavior differences after migration
- Mitigation: keep convenience wrappers thin; run existing test suites at each step
-
Binary body support gaps
- Mitigation:
nostr_http_request_t.bodyisunsigned char*with explicit length from day one
- Mitigation:
-
CA bundle detection regression on specific distros
- Mitigation: consolidate all 5 existing detection paths into one comprehensive function
-
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 APInostr_core/nostr_http.c— shared HTTP client implementationnostr_core/blossom_client.h— Blossom client APInostr_core/blossom_client.c— Blossom client implementation
nostr_core_lib (modified)
nostr_core/cashu_mint.c— replace static curl withnostr_httpnostr_core/nip005.c— replace static curl withnostr_httpnostr_core/nip011.c— replace static curl withnostr_httpnostr_core/nostr_core.h— add includes for new headersbuild.sh— add new source files
Didactyl (new)
src/tools/tool_blossom.c— Blossom tool implementations
Didactyl (modified)
src/llm.c— replace static curl withnostr_httpsrc/tools/tool_local.c— replace static curl withnostr_httpsrc/cashu_wallet.c— update CA bundle initsrc/main.c— addnostr_http_set_ca_bundle()call at startupsrc/tools/tools_internal.h— add Blossom prototypessrc/tools/tools_dispatch.c— add Blossom dispatchsrc/tools/tools_schema.c— add Blossom schemasMakefile— addtool_blossom.cto SRCSdocs/TOOLS.md— add Blossom section