Files
didactyl/plans/new_nostr_tools.md

18 KiB

Implementation Plan: Nostr Tools

Current Tool Inventory (v0.0.19)

# Tool Kind/NIP Status
1 nostr_post Any kind Shipped
2 nostr_post_readme 30023 / NIP-23 Shipped
3 nostr_query Filter-based Shipped
4 nostr_delete 5 / NIP-09 Shipped
5 nostr_react 7 / NIP-25 Shipped
6 nostr_profile_get 0 query Shipped
7 nostr_relay_status Pool stats Shipped
8 local_shell_exec OS Shipped
9 local_file_read OS Shipped
10 local_file_write OS Shipped

Pattern for All New Tools

Every tool follows the same three-step pattern in src/tools.c:

  1. Schema — Add OpenAI function schema block in tools_build_openai_schema_json()
  2. Executor — Implement static char* execute_<name>() function
  3. Dispatch — Add strcmp case in tools_execute()

Use the shared parse_tool_args_json() helper for hardened argument parsing.


Tier 2 Tools — Medium Value, Moderate Implementation

Tool 11: nostr_nip05_lookup — DNS Identity Verification (NIP-05)

Purpose

Verify user@domain.com NIP-05 identifiers. Useful for trust decisions and profile enrichment.

OpenAI Schema

{
  "name": "nostr_nip05_lookup",
  "description": "Verify or look up a NIP-05 DNS identifier and return the associated pubkey and relays",
  "parameters": {
    "type": "object",
    "properties": {
      "identifier": {
        "type": "string",
        "description": "NIP-05 identifier, e.g. user@domain.com"
      },
      "pubkey": {
        "type": "string",
        "description": "Optional 64-char hex pubkey to verify against the identifier"
      }
    },
    "required": ["identifier"]
  }
}

Implementation: execute_nostr_nip05_lookup()

1. Parse args_json
2. Extract "identifier" (required string, must contain @)
3. Extract "pubkey" (optional 64-char hex)
4. If pubkey provided:
   - Call nostr_nip05_verify(identifier, pubkey, &relays, &relay_count, 10)
   - Return { success, verified: true/false, pubkey, relays }
5. If no pubkey:
   - Call nostr_nip05_lookup(identifier, pubkey_out, &relays, &relay_count, 10)
   - Return { success, pubkey: pubkey_out, relays }
6. Free relay array after serializing

Library API Used

Files Modified

  • src/tools.c: Schema + executor + dispatch

Notes

  • Involves HTTP network I/O (fetches /.well-known/nostr.json from the domain)
  • May block for up to timeout_seconds — use 10s default
  • Must free the relays array returned by the library

Tool 12: nostr_encode — Bech32 Entity Encoding (NIP-19/NIP-21)

Purpose

Encode hex pubkeys, event IDs, or addressable coordinates into shareable npub, note, nprofile, nevent, naddr URIs.

OpenAI Schema

{
  "name": "nostr_encode",
  "description": "Encode a Nostr entity into a bech32 URI: npub, note, nprofile, nevent, or naddr",
  "parameters": {
    "type": "object",
    "properties": {
      "type": {
        "type": "string",
        "description": "Entity type: npub, note, nprofile, nevent, or naddr"
      },
      "hex": {
        "type": "string",
        "description": "64-char hex value: pubkey for npub/nprofile, event_id for note/nevent"
      },
      "relays": {
        "type": "array",
        "items": { "type": "string" },
        "description": "Optional relay hints for nprofile, nevent, naddr"
      },
      "kind": {
        "type": "integer",
        "description": "Kind number, required for naddr"
      },
      "identifier": {
        "type": "string",
        "description": "d-tag identifier, required for naddr"
      }
    },
    "required": ["type", "hex"]
  }
}

Implementation: execute_nostr_encode()

1. Parse args_json
2. Extract "type" (required: npub|note|nprofile|nevent|naddr)
3. Extract "hex" (required, 64-char hex)
4. Convert hex to 32-byte array via nostr_hex_to_bytes()
5. Switch on type:
   - "npub": nostr_build_uri_npub(bytes, output, sizeof output)
   - "note": nostr_build_uri_note(bytes, output, sizeof output)
   - "nprofile": nostr_build_uri_nprofile(bytes, relays, relay_count, output, sizeof output)
   - "nevent": nostr_build_uri_nevent(bytes, relays, relay_count, NULL, -1, 0, output, sizeof output)
   - "naddr": nostr_build_uri_naddr(identifier, bytes, kind, relays, relay_count, output, sizeof output)
6. Return { success, type, uri: output }

Library API Used

Files Modified

  • src/tools.c: Schema + executor + dispatch

Tool 13: nostr_decode — Bech32 Entity Decoding (NIP-19/NIP-21)

Purpose

Decode npub, note, nprofile, nevent, naddr URIs back into hex values and metadata.

OpenAI Schema

{
  "name": "nostr_decode",
  "description": "Decode a bech32 Nostr URI into its components: type, hex, relays, kind, identifier",
  "parameters": {
    "type": "object",
    "properties": {
      "uri": {
        "type": "string",
        "description": "Bech32 URI to decode, e.g. npub1..., note1..., nostr:npub1..."
      }
    },
    "required": ["uri"]
  }
}

Implementation: execute_nostr_decode()

1. Parse args_json
2. Extract "uri" (required string)
3. Call nostr_parse_uri(uri, &result)
4. Switch on result.type:
   - NPUB: convert result.data.pubkey to hex, return { type: "npub", pubkey: hex }
   - NOTE: convert result.data.event_id to hex, return { type: "note", event_id: hex }
   - NPROFILE: pubkey hex + relays array
   - NEVENT: event_id hex + relays + optional author/kind
   - NADDR: identifier + pubkey hex + kind + relays
5. Call nostr_uri_result_free(&result)
6. Return JSON

Library API Used

Files Modified

  • src/tools.c: Schema + executor + dispatch

Tool 14: nostr_dm_send — Send DM as Tool Call

Purpose

Expose DM sending as a tool so the agent can proactively message people (notify admin, reach out to contacts).

OpenAI Schema

{
  "name": "nostr_dm_send",
  "description": "Send an encrypted direct message to a Nostr user (NIP-04)",
  "parameters": {
    "type": "object",
    "properties": {
      "recipient_pubkey": {
        "type": "string",
        "description": "64-char hex pubkey of the recipient"
      },
      "message": {
        "type": "string",
        "description": "Plaintext message to send"
      }
    },
    "required": ["recipient_pubkey", "message"]
  }
}

Implementation: execute_nostr_dm_send()

1. Parse args_json
2. Extract "recipient_pubkey" (required, 64-char hex)
3. Extract "message" (required, non-empty string)
4. Call nostr_handler_send_dm(recipient_pubkey, message)
5. Return { success: true/false, recipient_pubkey, message_length }

Library API Used

Security Note

  • Should be admin-tier only in practice. The tool itself doesn't enforce tier, but the agent's system prompt and skill definitions should restrict usage.

Files Modified

  • src/tools.c: Schema + executor + dispatch

Tool 15: nostr_relay_info — Relay Information Document (NIP-11)

Purpose

Inspect relay capabilities, supported NIPs, limitations, PoW requirements before publishing.

OpenAI Schema

{
  "name": "nostr_relay_info",
  "description": "Fetch the NIP-11 relay information document for a relay URL",
  "parameters": {
    "type": "object",
    "properties": {
      "relay_url": {
        "type": "string",
        "description": "WebSocket relay URL, e.g. wss://relay.example.com"
      }
    },
    "required": ["relay_url"]
  }
}

Implementation

This requires a new helper in nostr_handler.c since NIP-11 fetching needs an HTTP GET request with Accept: application/nostr+json header.

New function: nostr_handler_relay_info_json()
1. Convert ws:// or wss:// URL to http:// or https://
2. Use libcurl to HTTP GET with header "Accept: application/nostr+json"
3. Return the raw JSON response string
execute_nostr_relay_info() in tools.c:
1. Parse args_json
2. Extract "relay_url" (required string, must start with ws:// or wss://)
3. Call nostr_handler_relay_info_json(relay_url)
4. Parse returned JSON
5. Return { success, relay_url, info: { name, description, pubkey, supported_nips, limitations, ... } }

Files Modified

  • src/nostr_handler.h: Add nostr_handler_relay_info_json() declaration
  • src/nostr_handler.c: Implement using libcurl (already linked)
  • src/tools.c: Schema + executor + dispatch

Tier 3 Tools — High Value, More Complex

Tool 16: nostr_dm_send_nip17 — Private DMs via Gift Wrap (NIP-17/NIP-59)

Purpose

Send modern, metadata-private DMs using the NIP-17 gift wrap protocol instead of legacy NIP-04.

OpenAI Schema

{
  "name": "nostr_dm_send_nip17",
  "description": "Send a private direct message using NIP-17 gift wrap protocol for metadata privacy",
  "parameters": {
    "type": "object",
    "properties": {
      "recipient_pubkey": {
        "type": "string",
        "description": "64-char hex pubkey of the recipient"
      },
      "message": {
        "type": "string",
        "description": "Plaintext message to send"
      },
      "subject": {
        "type": "string",
        "description": "Optional conversation subject"
      }
    },
    "required": ["recipient_pubkey", "message"]
  }
}

Implementation

This is a multi-step crypto pipeline. The library handles the heavy lifting but we need to orchestrate:

New function: nostr_handler_send_dm_nip17() in nostr_handler.c
1. Query recipient's kind 10050 relay list (DM relays)
   - Build filter: { kinds: [10050], authors: [recipient_pubkey], limit: 1 }
   - Call nostr_relay_pool_query_sync()
   - Extract relay URLs via nostr_nip17_extract_dm_relays()
   - Fall back to our own relays if no 10050 found
2. Create kind 14 chat event:
   - nostr_nip17_create_chat_event(message, &recipient_pubkey, 1, subject, NULL, NULL, our_pubkey)
3. Gift wrap and send:
   - nostr_nip17_send_dm(chat_event, &recipient_pubkey, 1, our_private_key, gift_wraps, max_wraps, 0)
4. Publish each gift wrap to recipient's DM relays
5. Return success/failure + event metadata
execute_nostr_dm_send_nip17() in tools.c:
1. Parse args_json
2. Extract recipient_pubkey, message, optional subject
3. Call nostr_handler_send_dm_nip17(recipient_pubkey, message, subject)
4. Return { success, recipient_pubkey, protocol: "nip17" }

Library API Used

Files Modified

  • src/nostr_handler.h: Add nostr_handler_send_dm_nip17() declaration
  • src/nostr_handler.c: Implement the full NIP-17 send pipeline
  • src/tools.c: Schema + executor + dispatch

Tool 17: nostr_encrypt — NIP-44 Encryption

Purpose

Encrypt arbitrary content for a specific recipient using modern NIP-44 encryption.

OpenAI Schema

{
  "name": "nostr_encrypt",
  "description": "Encrypt plaintext for a recipient using NIP-44 ChaCha20+HMAC encryption",
  "parameters": {
    "type": "object",
    "properties": {
      "recipient_pubkey": {
        "type": "string",
        "description": "64-char hex pubkey of the recipient"
      },
      "plaintext": {
        "type": "string",
        "description": "Text to encrypt"
      }
    },
    "required": ["recipient_pubkey", "plaintext"]
  }
}

Implementation: execute_nostr_encrypt()

1. Parse args_json
2. Extract recipient_pubkey (64-char hex), plaintext (non-empty string)
3. Convert recipient_pubkey hex to 32-byte array
4. Allocate output buffer (plaintext length * 2 + 256 for base64 overhead)
5. Call nostr_nip44_encrypt(our_private_key, recipient_pubkey_bytes, plaintext, output, output_size)
6. Return { success, recipient_pubkey, ciphertext: output }

Library API Used

Security Note

  • Admin-tier only. Uses the agent's private key for ECDH shared secret.

Files Modified

  • src/tools.c: Schema + executor + dispatch

Tool 18: nostr_decrypt — NIP-44 Decryption

Purpose

Decrypt NIP-44 encrypted content from a known sender.

OpenAI Schema

{
  "name": "nostr_decrypt",
  "description": "Decrypt NIP-44 encrypted content from a sender",
  "parameters": {
    "type": "object",
    "properties": {
      "sender_pubkey": {
        "type": "string",
        "description": "64-char hex pubkey of the sender"
      },
      "ciphertext": {
        "type": "string",
        "description": "Base64-encoded NIP-44 encrypted payload"
      }
    },
    "required": ["sender_pubkey", "ciphertext"]
  }
}

Implementation: execute_nostr_decrypt()

1. Parse args_json
2. Extract sender_pubkey (64-char hex), ciphertext (non-empty string)
3. Convert sender_pubkey hex to 32-byte array
4. Allocate output buffer
5. Call nostr_nip44_decrypt(our_private_key, sender_pubkey_bytes, ciphertext, output, output_size)
6. Return { success, sender_pubkey, plaintext: output }

Library API Used

Security Note

  • Admin-tier only.

Files Modified

  • src/tools.c: Schema + executor + dispatch

Tool 19: nostr_list_manage — List Management (NIP-51)

Purpose

Manage mute lists (kind 10000), bookmarks (kind 10003), follow sets, relay sets. Lets the agent curate its social graph.

OpenAI Schema

{
  "name": "nostr_list_manage",
  "description": "Add or remove items from a Nostr list: mute list, bookmarks, relay sets, etc. (NIP-51)",
  "parameters": {
    "type": "object",
    "properties": {
      "list_kind": {
        "type": "integer",
        "description": "Kind number of the list: 10000=mute, 10001=pins, 10003=bookmarks, 3=follows, 10002=relays"
      },
      "action": {
        "type": "string",
        "description": "Action: add or remove"
      },
      "items": {
        "type": "array",
        "items": {
          "type": "array",
          "items": { "type": "string" }
        },
        "description": "Array of tag tuples to add/remove, e.g. [[p, pubkey], [e, event_id]]"
      }
    },
    "required": ["list_kind", "action", "items"]
  }
}

Implementation

This is a read-modify-write pattern for replaceable events:

execute_nostr_list_manage():
1. Parse args_json
2. Extract list_kind (integer), action ("add"|"remove"), items (array of tag arrays)
3. Query existing list:
   - Build filter: { kinds: [list_kind], authors: [our_pubkey], limit: 1 }
   - Call nostr_handler_query_json(filter, 8000)
4. Parse existing event tags (or start with empty array if none found)
5. If action == "add":
   - For each item in items: append to tags if not already present
6. If action == "remove":
   - For each item in items: remove matching tag from tags
7. Publish updated event:
   - Call nostr_handler_publish_kind_event(list_kind, existing_content_or_empty, updated_tags, &result)
8. Return { success, list_kind, action, items_affected, event_id }

Notes

  • Replaceable events (kind 10000-19999) automatically replace previous versions
  • Kind 3 (follows) is also replaceable
  • Content field may contain NIP-44 encrypted private items — preserve it unchanged
  • Must deduplicate tags when adding

Files Modified

  • src/tools.c: Schema + executor + dispatch

Implementation Order

graph TD
    A[nostr_nip05_lookup] --> B[nostr_encode]
    B --> C[nostr_decode]
    C --> D[nostr_dm_send]
    D --> E[nostr_relay_info]
    E --> F[nostr_encrypt]
    F --> G[nostr_decrypt]
    G --> H[nostr_dm_send_nip17]
    H --> I[nostr_list_manage]
    
    style A fill:#FFFFE0
    style B fill:#FFFFE0
    style C fill:#FFFFE0
    style D fill:#FFFFE0
    style E fill:#FFFFE0
    style F fill:#FFD700
    style G fill:#FFD700
    style H fill:#FFD700
    style I fill:#FFD700

Yellow = Tier 2 (moderate), Gold = Tier 3 (complex).

Batch A — Pure computation, no new handler functions:

  1. nostr_encode — pure NIP-21 encoding
  2. nostr_decode — pure NIP-21 decoding
  3. nostr_dm_send — thin wrapper around existing nostr_handler_send_dm()

Batch B — Network I/O tools: 4. nostr_nip05_lookup — HTTP fetch via library 5. nostr_relay_info — HTTP fetch via new handler function + libcurl

Batch C — Crypto tools: 6. nostr_encrypt — NIP-44 encrypt 7. nostr_decrypt — NIP-44 decrypt

Batch D — Complex orchestration: 8. nostr_dm_send_nip17 — multi-step NIP-17/59 pipeline, new handler function 9. nostr_list_manage — read-modify-write replaceable events

Build & Validate

After each batch:

  1. Run ./build_static.sh to verify compilation
  2. Run ./increment_and_push.sh with descriptive message