From 5efe932a5fe30c9babf778fbc9e3cf23ef007ce0 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sat, 21 Mar 2026 19:26:02 -0400 Subject: [PATCH] v0.1.20 - Complete Blossom hardening and validation: HTTPS-only Blossom tools, config-driven upload/download limits, overwrite protection, CA bundle unification via nostr_http, and comprehensive live+mock nostr_http/blossom test coverage --- Makefile | 1 + README.md | 4 +- config.jsonc.example | 2 + docs/TOOLS.md | 10 + plans/blossom_tools.md | 668 +++++++++++++++++++++++++++ src/cashu_wallet.c | 28 +- src/config.c | 18 + src/config.h | 2 + src/llm.c | 137 ++---- src/main.c | 28 +- src/main.h | 4 +- src/nostr_handler.c | 62 +++ src/nostr_handler.h | 1 + src/setup_wizard.c | 2 + src/tools/tool_blossom.c | 304 ++++++++++++ src/tools/tool_local.c | 177 ++----- src/tools/tools_dispatch.c | 15 + src/tools/tools_internal.h | 6 + src/tools/tools_schema.c | 98 ++++ tests/blossom_tool_validation_test | Bin 0 -> 166712 bytes tests/blossom_tool_validation_test.c | 79 ++++ 21 files changed, 1380 insertions(+), 266 deletions(-) create mode 100644 plans/blossom_tools.md create mode 100644 src/tools/tool_blossom.c create mode 100755 tests/blossom_tool_validation_test create mode 100644 tests/blossom_tool_validation_test.c diff --git a/Makefile b/Makefile index 07934cb..742d603 100644 --- a/Makefile +++ b/Makefile @@ -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 \ diff --git a/README.md b/README.md index 8d90069..debd70b 100644 --- a/README.md +++ b/README.md @@ -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.20 **Active build — this project is barely working. Experiment at your own risk.** -> Last release update: v0.1.19 — Prevent ignored spent token events from reloading into wallet balance after restart +> Last release update: v0.1.20 — Complete Blossom hardening and validation: HTTPS-only Blossom tools, config-driven upload/download limits, overwrite protection, CA bundle unification via nostr_http, and comprehensive live+mock nostr_http/blossom test coverage - Connects to configured relays with auto-reconnect and relay state transition logging - Publishes configured startup events per relay as each relay becomes connected diff --git a/config.jsonc.example b/config.jsonc.example index 7d50974..5680279 100644 --- a/config.jsonc.example +++ b/config.jsonc.example @@ -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, diff --git a/docs/TOOLS.md b/docs/TOOLS.md index ef99981..7825023 100644 --- a/docs/TOOLS.md +++ b/docs/TOOLS.md @@ -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 | diff --git a/plans/blossom_tools.md b/plans/blossom_tools.md new file mode 100644 index 0000000..d5b527c --- /dev/null +++ b/plans/blossom_tools.md @@ -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 + +// 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 ` 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 ` 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 + +// 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/" +} +``` + +### `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": "", + "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 ` 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
Single curl implementation] + NIP1[nip001.c
Event signing] + UTIL[utils.c
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 diff --git a/src/cashu_wallet.c b/src/cashu_wallet.c index 77b19ba..ecedda0 100644 --- a/src/cashu_wallet.c +++ b/src/cashu_wallet.c @@ -10,7 +10,6 @@ #include #include -#include #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) { diff --git a/src/config.c b/src/config.c index 93673d0..670a981 100644 --- a/src/config.c +++ b/src/config.c @@ -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; diff --git a/src/config.h b/src/config.h index 1cdb630..90d9e38 100644 --- a/src/config.h +++ b/src/config.h @@ -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 { diff --git a/src/llm.c b/src/llm.c index f6cc804..f676b7b 100644 --- a/src/llm.c +++ b/src/llm.c @@ -2,7 +2,6 @@ #include "llm.h" -#include #include #include #include @@ -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; } diff --git a/src/main.c b/src/main.c index 4e02cb6..ec38c63 100644 --- a/src/main.c +++ b/src/main.c @@ -7,7 +7,7 @@ #include #include -#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); diff --git a/src/main.h b/src/main.h index 769294c..d745025 100644 --- a/src/main.h +++ b/src/main.h @@ -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 20 +#define DIDACTYL_VERSION "v0.1.20" // Agent metadata #define DIDACTYL_NAME "Didactyl" diff --git a/src/nostr_handler.c b/src/nostr_handler.c index 0f750ef..a0df91d 100644 --- a/src/nostr_handler.c +++ b/src/nostr_handler.c @@ -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; diff --git a/src/nostr_handler.h b/src/nostr_handler.h index 5d89562..976ed3e 100644 --- a/src/nostr_handler.h +++ b/src/nostr_handler.h @@ -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); diff --git a/src/setup_wizard.c b/src/setup_wizard.c index 294bbc8..856c64d 100644 --- a/src/setup_wizard.c +++ b/src/setup_wizard.c @@ -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; diff --git a/src/tools/tool_blossom.c b/src/tools/tool_blossom.c new file mode 100644 index 0000000..8c1a6a3 --- /dev/null +++ b/src/tools/tool_blossom.c @@ -0,0 +1,304 @@ +#define _POSIX_C_SOURCE 200809L + +#include "tools_internal.h" + +#include +#include +#include +#include + +#include +#include + +#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; +} diff --git a/src/tools/tool_local.c b/src/tools/tool_local.c index ba38c57..3a74ca4 100644 --- a/src/tools/tool_local.c +++ b/src/tools/tool_local.c @@ -2,7 +2,6 @@ #include "tools_internal.h" -#include #include #include #include @@ -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; } diff --git a/src/tools/tools_dispatch.c b/src/tools/tools_dispatch.c index 56162ec..b4bfa51 100644 --- a/src/tools/tools_dispatch.c +++ b/src/tools/tools_dispatch.c @@ -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"); } diff --git a/src/tools/tools_internal.h b/src/tools/tools_internal.h index 39dff27..d15ed6a 100644 --- a/src/tools/tools_internal.h +++ b/src/tools/tools_internal.h @@ -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); diff --git a/src/tools/tools_schema.c b/src/tools/tools_schema.c index e4e7bda..317275e 100644 --- a/src/tools/tools_schema.c +++ b/src/tools/tools_schema.c @@ -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; diff --git a/tests/blossom_tool_validation_test b/tests/blossom_tool_validation_test new file mode 100755 index 0000000000000000000000000000000000000000..dd1a4cc99fa82c2ffd7c385020f0bd642334b6ac GIT binary patch literal 166712 zcmeF)dwdgB9zXmkP>2^yRI0%n5vv9TOoeJukd%u|wO|y`x_~X#*0OTxmK36h2HRp{ z?QV2+t?Q~)ch~BAsW-NGja=)xY6LG;yu=GQ-HKFHtfKb$o|#Y5>1XZl^?N-lfH z3(0xUcg~!1=FFLMCMjM&z3|)t85vsYXP|bDrc$0xOZ?c7S2FNQnD^{V#k@2B-$YfI`?b6IJ%U#5DS zRDa1&()>I9`t|cxI`wMb&pcieN3l1`XZA5M8XJyqVR7}XxN+8X)W z8j#!+vGcm2%);^A9BU*Zk&wyrmz1 zq@Uwf^>f_C{p53=G{mo!=lA{O|3N>#ub=$y>BrafQ%-eV{#tng{lp*IPx-IzC;ps% z{Ly}VZ9n-J^%Fm%pYng-PyGG;mzM>XRobDltg3uj)e?Iy3o9$jmR@1o>B_R;qNU}_ zwB==`m8a*PaaB%n>9VE4vQSWJvpiU}>}uPVveM$p>dUVxySlh?Vb$`oU)@q#Rkko_ z-?Y56V(GG~s^yEWES5h@$DMB5EwpUuV#(~+^X#*6d0Ew>g^L$mW6xQNBc)cwSs`1? z?IjhgT)3#JIGu;>psD)(S4n%Tx3uiazLn_RRJv@*<%^d7dNG14%NAZ$yzq)EY)9+e z^Vh17D(8QdB~{3(g-fqkwxn;pZ7up=6)!8jqT*MNZk26G^_ zR#m)sS*fzn!s7BpORY98F0QOvv@}>=Tv~CJCcU7vqDosLeNQ6GmzR|(TNsp`WVR{i z!0gvc7A{(9hAyeps>+ruTT!N!FO$Bem6tAFruvHHQYoh>SQT6*?W11Itfds%lCmYz zcg>K6az-Tw*}hc078fsHYAZ^4uuLuhbtR|>`M(ut<-1r8CcU|A=?d9RvXsM_<(Cd= zmOPb{%G8#p6{#{^XqKzgtl6UF%StCqC|VRVvrw@`a1! zQnYejSXx@Pe7P1}w4_W^O(K`EnsSd8a>W~K*g7a{$$naFT2v9iUbpRY(VitMmZvI!UXa)Ot3x zj?})iJcM;_%Hw#B5GLR9u-steCqn)qJVyRyixuBUz7z3H+cr*EH@D}nf;>|_NRppQ^1+A?ksk~XlOG0;kRJ(;ksk+dBtHq>L_P-IO#bi| z^ndczPgvecJ|6kR$tS_v$fv;*hkv|VFA>RnEAa8?L zlD`2Dk-rNMlYb14kne=Y$X&RfHj;NDzKOgG-b~&NZz2B$-by|g_xCvYLGU(m7d$~e z9Ntbo0-hv45pIx=f_IVUz`MyOz%@_b{(lbKK|USsA}@ry$!Eho)}o0 zad#HUB5S1@W!q@3dO|Do*|<#J7>( z1y7JS!Q09I22Yaz18$JFz`MxT!Mn*{fNLZB_WwRQ z8_6fYo5-iYo5>5{E#v`sEBVFnIQeDpHu5Xr3G&~<+sS_iPm-^M8|2r)yU1_)H^zVR zwTRbF=-dD6;STZ`+(mu|+)aKD+(Z61xR?A9xR3lPcpmwBxS#w5xK6$W9w2`eUPQhP zUP9guuOR;rUP-m#J>Ii2)KiM#Ix4*?qWy0oBTw$hkOj&OTOh*D}NvPc*N(CPk{T$ zC&P8}>F@w~A-stELU;*z5xj!@GI%9v4yp{Z~@HqLy@HX}q|$Rj@x?k9J_b@C(N0rF$vMdTylCFEXs1^F0wCHXjb zh zN8twfE$}Y#JK){q_rSH2`}Y6)aes7>{~hrz^2gwA@~7b*^5@`Q@{MpG`K$0e^6hXx z`3|^F{y98A{w2JK{5yCF`OokQ^1)AA_peIwgWw@@7d%XUBs@ZX3_M0Y65dFDGQ5d= zG`yL7JiLW`0=$*n508_d3vVNz1y7LAg}0N>hbPI8!{;3ac`4$%$bSp(Ca;8R-oE{R zCEP*28tx*$0q!P`z&+%@hkMCyhx^Fyg6EOn2ltad2-nFUg9pf;h8K~qhnJAQ46h*H z3a=!84IUzY3mzt)jqjgC$lpVJj64Z%B;N&ZBL52BO#Usrg?vA}m3$zcm&VBtgtw8q z;0f|!@OJWJ;7Ri1;Rg9Aco+E?csKbOaBWoI{(lbKL4GdWMLrAeCZ7ZMkk5yE$tPod z;Uj+>&oT1IOOcPC{7Se^z7!rHUk)!KzZzab9)?$t-w3ZHzZo7Pza1VXzw0IIJ`y4S zGvZ_9_re>={|;{=e-z$K{xrOW`~`R``6hUr{NM04^0(m$@{i!{%HBEJw`LS6)~AYTZtB(H#n$d|#x4v hBuNghc}U53vVV5!&}Jf;H~5h@HqKx@HXx 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

g;1qE7&1zdfczXM1?vb?PX931e2~FRSdy2&h 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 literal 0 HcmV?d00001 diff --git a/tests/blossom_tool_validation_test.c b/tests/blossom_tool_validation_test.c new file mode 100644 index 0000000..edae540 --- /dev/null +++ b/tests/blossom_tool_validation_test.c @@ -0,0 +1,79 @@ +/* + * Didactyl Blossom tool argument validation tests. + */ + +#include +#include +#include + +#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; +}