upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/PLAN.md
diff options
context:
space:
mode:
authorYour Name <you@example.com>2026-05-18 03:37:27 +0530
committerYour Name <you@example.com>2026-05-18 03:37:27 +0530
commit8a2f7a6c9423e0c00fae3c1233bee9e0bb3ae239 (patch)
tree8f8d2ede379b7e3cc0da82d472bcf0eeedcbf03b /PLAN.md
parentfe7c3be2fd9d464dbc837d1913409d2691bd50f5 (diff)
feat: ContextVM (MCP over Nostr) server with WS masking fix
- Full CVM server: persistent WS relay listener, kind 25910 subscription - MCP protocol handlers: initialize, tools/list, tools/call, ping - 10 MCP tools: get_config, set_config, get_balance, wallet_send, get_sessions, get_usage, set_payout, set_metric, set_price, wallet_melt - CEP-6 announcements via WS (kinds 11316, 11317, 10002) - Auth check: owner npub only - Fix: WebSocket client-to-server frame masking (RFC 6455 requirement) - Fix: Raw event JSON in EVENT wrapper (no re-parsing that breaks sig) - SNTP init after STA gets IP - 282 unit tests passing (61 CVM + 60 MCP handler + 161 existing) - Integration test scaffold: tests/integration/test-cvm.mjs
Diffstat (limited to 'PLAN.md')
-rw-r--r--PLAN.md175
1 files changed, 151 insertions, 24 deletions
diff --git a/PLAN.md b/PLAN.md
index 5690c1b..5bd12f9 100644
--- a/PLAN.md
+++ b/PLAN.md
@@ -473,21 +473,48 @@ uint64_t bytes_consumed;
473| 51 | NAPT byte counting | Integration | Counters match actual traffic | TODO | 473| 51 | NAPT byte counting | Integration | Counters match actual traffic | TODO |
474| 52 | Bytes metric end-to-end | E2E | Client disconnected after data cap | TODO | 474| 52 | Bytes metric end-to-end | E2E | Client disconnected after data cap | TODO |
475 475
476### Phase 7: ContextVM Server (MCP over Nostr) — COMPLETE 476### Phase 7: ContextVM Server (MCP over Nostr) — REWRITE IN PROGRESS
477 477
478**Goal:** Remote configuration of ESP32 TollGate via ContextVM — services communicate over Nostr using public keys as addresses. Exposes configuration as MCP tools accessible by both humans and AI agents. 478**Goal:** Full ContextVM protocol implementation — ESP32 acts as an MCP server discoverable on the Nostr network via CEP-6 public announcements, communicating via kind 25910 ephemeral events.
479 479
480**New files:** `main/cvm_server.c`, `main/cvm_server.h`, `main/nip44.c`, `main/nip44.h`, `main/mcp_handler.c`, `main/mcp_handler.h` 480**Protocol:** ContextVM transports MCP JSON-RPC 2.0 messages over Nostr. Server is identified by its npub (derived from nsec). Clients discover the server via kind 11316 announcements, then communicate via kind 25910 ephemeral events.
481 481
482#### Architecture 482#### Architecture
483 483
484ContextVM uses MCP (JSON-RPC 2.0) over NIP-44 encrypted Nostr DMs: 484```
4851. ESP32 subscribes to Nostr relays for DMs addressed to its npub 485Client (nak/ContextVM SDK)
4862. Incoming DMs are NIP-44 decrypted, parsed as MCP JSON-RPC requests 486 → publishes kind 25910 to relay ({"method":"tools/call","params":{"name":"get_config"}})
4873. Dispatched to registered tool handlers 487 → ESP32 cvm_server reads from persistent WebSocket subscription
4884. Responses sent back via NIP-44 encrypted DM 488 → parses MCP JSON-RPC from event content
489 → dispatches to mcp_handler.c
490 → publishes kind 25910 response back to relay
491 → client receives response via subscription
492```
493
494#### ContextVM Event Kinds Used
495
496| Kind | Purpose | CEP |
497|------|---------|-----|
498| 25910 | MCP request/response transport (ephemeral) | Draft spec |
499| 11316 | Server announcement (replaceable) | CEP-6 |
500| 11317 | Tools list announcement (replaceable) | CEP-6 |
501| 10002 | Relay list (replaceable) | CEP-17 (NIP-65) |
502
503#### MCP Protocol Flow
489 504
490#### MCP Tools Exposed 5051. ESP32 publishes kind 11316 (server announcement) + kind 11317 (tools list) + kind 10002 (relay list) on startup
5062. ESP32 opens persistent WebSocket to relays, subscribes to `{"kinds":[25910],"#p":["<npub>"]}`
5073. Client sends kind 25910 `initialize` request
5084. ESP32 responds with kind 25910 `initialize` result (capabilities, serverInfo)
5095. Client sends `notifications/initialized`
5106. Client calls `tools/list` or `tools/call`
5117. ESP32 dispatches to `mcp_handler.c`, returns result
512
513#### Encryption
514
515Phase 7a ships with **plaintext** kind 25910 events. Encryption (CEP-4: NIP-44 gift wrap) is deferred to Phase 7b. The `support_encryption` tag is NOT included in announcements until Phase 7b.
516
517#### MCP Tools Exposed (10 total)
491 518
492| Tool | Input | Output | 519| Tool | Input | Output |
493|------|-------|--------| 520|------|-------|--------|
@@ -497,32 +524,60 @@ ContextVM uses MCP (JSON-RPC 2.0) over NIP-44 encrypted Nostr DMs:
497| `get_sessions` | — | Array of active sessions | 524| `get_sessions` | — | Array of active sessions |
498| `get_usage` | — | Upstream usage if client active | 525| `get_usage` | — | Upstream usage if client active |
499| `set_payout` | `{recipients: [...]}` | Success/error | 526| `set_payout` | `{recipients: [...]}` | Success/error |
500| `set_metric` | `{"bytes" or "milliseconds"}` | Success/error | 527| `set_metric` | `{"metric": "bytes" or "milliseconds"}` | Success/error |
501| `set_price` | `{price_per_step: N}` | Success/error | 528| `set_price` | `{"price_per_step": N}` | Success/error |
502| `wallet_send` | `{amount_sats: N}` | `{token: "cashuA..."}` | 529| `wallet_send` | `{"amount": N}` | `{token: "cashuA..."}` |
503| `wallet_melt` | `{bolt11: "ln..."}` | `{preimage: "..."}` | 530| `wallet_melt` | `{"bolt11": "ln..."}` | `{preimage: "..."}` |
504 531
505#### Auth 532#### Auth
506 533
507Only accept commands from owner npub (derived from nsec in config.json). 534Only accept kind 25910 requests from owner npub (derived from nsec in config.json). Non-owner requests are silently dropped.
508 535
509#### Dependencies 536#### Dependencies
510 537
511- XChaCha20-Poly1305 (from mbedtls or libsodium) 538- WebSocket persistent connection (extends `wifistr.c` TLS + WS pattern)
512- Base64url encoding (already in cashu code) 539- secp256k1 Schnorr signing (existing `nostr_event.c`)
513- WebSocket listener (extends existing wifistr infrastructure) 540- cJSON (existing)
514- NIP-44 v2 encryption/decryption 541- mbedtls TLS (existing)
542- NIP-04 encryption (existing `nip04.c`) — for future encrypted mode
543
544#### Files
545
546| File | Status | Purpose |
547|------|--------|---------|
548| `main/cvm_server.c` | Rewrite | WS listener, MCP handlers, CEP-6 announcements |
549| `main/cvm_server.h` | Update | New public API |
550| `main/mcp_handler.c` | Extend | 6 new tools |
551| `main/mcp_handler.h` | Update | New tool enums + handlers |
552| `main/config.c` | Minor | Default `cvm_enabled = true` |
553| `tests/unit/test_cvm_server.c` | New | CVM unit tests |
554| `tests/unit/test_mcp_handler.c` | Extend | 6 new tool tests |
555| `tests/integration/test-cvm.mjs` | New | CVM integration test via nak |
556| `Makefile` | Update | `cvm-*` targets |
515 557
516#### Test Cases 558#### Test Cases
517 559
518| # | Test | Method | Pass Criteria | Status | 560| # | Test | Method | Pass Criteria | Status |
519|---|------|--------|---------------|--------| 561|---|------|--------|---------------|--------|
520| 53 | NIP-44 encrypt/decrypt | Unit test | Roundtrip matches | TODO | 562| 53 | MCP JSON-RPC parse from kind 25910 | Unit test | Correct dispatch to tool handler | TODO |
521| 54 | MCP JSON-RPC parse | Unit test | Correct dispatch | TODO | 563| 54 | Kind 11316 announcement construction | Unit test | Valid event with correct tags/capabilities | TODO |
522| 55 | Config change via DM | Integration | ESP32 applies new config | TODO | 564| 55 | Kind 11317 tools list construction | Unit test | All 10 tools listed with schemas | TODO |
523| 56 | Balance query via CVM | Integration | Returns correct balance | TODO | 565| 56 | Kind 10002 relay list construction | Unit test | Correct `r` tags | TODO |
524 566| 57 | Auth rejection for non-owner | Unit test | Non-owner events dropped | TODO |
525## Total: 56 Tests across 7 phases 567| 58 | MCP initialize response | Unit test | Correct capabilities + serverInfo | TODO |
568| 59 | New tool: get_sessions | Unit test | Returns session array | TODO |
569| 60 | New tool: get_usage | Unit test | Returns usage stats | TODO |
570| 61 | New tool: set_payout | Unit test | Updates payout config | TODO |
571| 62 | New tool: set_metric | Unit test | Updates metric field | TODO |
572| 63 | New tool: set_price | Unit test | Updates price_per_step | TODO |
573| 64 | New tool: wallet_melt | Unit test | Calls nucula_wallet_melt | TODO |
574| 65 | Kind 11316 on relay | Integration | Announcement found on relay | TODO |
575| 66 | MCP initialize roundtrip | Integration | Response received via nak | TODO |
576| 67 | get_config via CVM | Integration | Returns valid JSON config | TODO |
577| 68 | get_balance via CVM | Integration | Returns balance + proofs | TODO |
578| 69 | set_price via CVM | Integration | Price updated on device | TODO |
579
580## Total: 78 Tests across 8 phases
526 581
527## Post-Phase 7: Bug Fixes & Architecture Improvements 582## Post-Phase 7: Bug Fixes & Architecture Improvements
528 583
@@ -591,6 +646,78 @@ int tollgate_ip4_canforward_filter(struct pbuf *p, u32_t dest_addr_hostorder) {
591 646
592**Rationale:** The mint's `cashu_check_proof_states()` already catches double-spends over HTTP. `nucula_wallet_receive()` → `swap()` registers proofs as spent and replaces them. After a successful receive, the old token is useless. Local tracking adds no security, wastes 6.5KB RAM, and is lost on reboot anyway. 647**Rationale:** The mint's `cashu_check_proof_states()` already catches double-spends over HTTP. `nucula_wallet_receive()` → `swap()` registers proofs as spent and replaces them. After a successful receive, the old token is useless. Local tracking adds no security, wastes 6.5KB RAM, and is lost on reboot anyway.
593 648
649### Phase 8: TFT Display (JC3248W535 / AXS15231B) — IN PROGRESS
650
651**Goal:** Add TFT display support to the JC3248W535 board for QR code rendering + status text. Display cycles between a Wi-Fi QR code (so customers can connect) and a portal URL QR code (for direct portal access).
652
653**Hardware:** JC3248W535 board — ESP32-S3, AXS15231B 320x480 QSPI TFT, capacitive touch
654**Pin mapping:** CS=45, CLK=47, D0=21, D1=48, D2=40, D3=39, BL=1, Touch SDA=4, Touch SCL=8
655
656#### Components Created
657
658| Component | Path | Purpose |
659|-----------|------|---------|
660| `components/qrcode/` | `qrcoded.c/h` + CMakeLists.txt | QR code generation (ported from NSD, MIT license) |
661| `components/axs15231b/` | `axs15231b.c/h` + CMakeLists.txt | AXS15231B QSPI display driver |
662| `main/display.c/h` | Display abstraction | FreeRTOS display task, state machine, QR cycling |
663| `main/font.c/h` | 8x8 bitmap font | ASCII 32-127 for status text rendering |
664
665#### Display States
666
667| State | Screen | QR Content |
668|-------|--------|------------|
669| `DISPLAY_BOOT` | "TollGate starting..." | None |
670| `DISPLAY_READY` | QR code + SSID label | Cycles: Wi-Fi QR ↔ Portal URL QR every 5s |
671| `DISPLAY_PAYMENT_RECEIVED` | Green "Paid! Access granted" | None (2s, then READY) |
672| `DISPLAY_ERROR` | Red "No upstream" | None |
673
674#### Wi-Fi QR Code Format
675
676Uses the standardized ZXing `WIFI:` URI scheme — natively recognized by Android and iOS camera apps:
677
678```
679WIFI:S:<escaped_ssid>;T:nopass;;
680```
681
682**Special character escaping**: `;`, `:`, `\`, `,`, `"` are backslash-escaped in the SSID field per spec.
683
684**Example:**
685```
686SSID: TollGate-C0E9CA → WIFI:S:TollGate-C0E9CA;T:nopass;;
687SSID: My;WiFi:Test → WIFI:S:My\;WiFi\:Test;T:nopass;;
688```
689
690When scanned, the phone **automatically connects** to the TollGate AP — then the captive portal takes over for payment.
691
692#### QR Cycling
693
694The display alternates between two QR modes every 5 seconds:
6951. **Wi-Fi QR** — `WIFI:S:...;T:nopass;;` — auto-connects phone to AP
6962. **Portal URL QR** — `http://10.x.x.1/` — direct link to captive portal
697
698Label text below QR changes to indicate current mode: "Scan to connect" vs "Portal URL".
699
700#### Display Driver Architecture
701
702- **Interface**: Single-line SPI (MOSI on D0/GPIO21) — simpler than QSPI, reliable for V1
703- **Framebuffer**: 307,200 bytes (480x320x2 RGB565) in PSRAM via `heap_caps_malloc`
704- **Flush**: 10 chunks of 32KB via `spi_device_polling_transmit`
705- **Rotation**: Landscape (MADCTL=0x60, MX|MV)
706- **Backlight**: GPIO1 active-high
707
708#### Test Cases
709
710| # | Test | Method | Pass Criteria | Status |
711|---|------|--------|---------------|--------|
712| 70 | Wi-Fi QR scannable | Android camera scan | Phone connects to AP | TODO |
713| 71 | Portal URL QR scannable | Android camera scan | Browser opens portal | TODO |
714| 72 | QR cycling | Watch display | Mode changes every 5s | TODO |
715| 73 | Boot screen | Visual | "TollGate starting..." shown | TODO |
716| 74 | Payment screen | Trigger payment | Green "Paid!" for 2s | TODO |
717| 75 | Error screen | Disconnect upstream | Red "No upstream" | TODO |
718| 76 | Special char escape | Unit test | `\;:,"` correctly escaped | TODO |
719| 77 | QR generation | Unit test | Valid QR matrix for various string lengths | TODO |
720
594## Testing Infrastructure 721## Testing Infrastructure
595 722
596### Three-Layer Test Architecture 723### Three-Layer Test Architecture