Files
didactyl/plans/blossom_tools.md

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 API
  • nostr_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

  1. Binary-safe bodybody 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 capmax_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 wrappersnostr_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:

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.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:

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

#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:

{
  "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

  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

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