diff options
| author | Your Name <you@example.com> | 2026-05-18 03:37:27 +0530 |
|---|---|---|
| committer | Your Name <you@example.com> | 2026-05-18 03:37:27 +0530 |
| commit | 8a2f7a6c9423e0c00fae3c1233bee9e0bb3ae239 (patch) | |
| tree | 8f8d2ede379b7e3cc0da82d472bcf0eeedcbf03b /PLAN.md | |
| parent | fe7c3be2fd9d464dbc837d1913409d2691bd50f5 (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.md | 175 |
1 files changed, 151 insertions, 24 deletions
| @@ -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 | ||
| 484 | ContextVM uses MCP (JSON-RPC 2.0) over NIP-44 encrypted Nostr DMs: | 484 | ``` |
| 485 | 1. ESP32 subscribes to Nostr relays for DMs addressed to its npub | 485 | Client (nak/ContextVM SDK) |
| 486 | 2. 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"}}) |
| 487 | 3. Dispatched to registered tool handlers | 487 | → ESP32 cvm_server reads from persistent WebSocket subscription |
| 488 | 4. 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 | 505 | 1. ESP32 publishes kind 11316 (server announcement) + kind 11317 (tools list) + kind 10002 (relay list) on startup |
| 506 | 2. ESP32 opens persistent WebSocket to relays, subscribes to `{"kinds":[25910],"#p":["<npub>"]}` | ||
| 507 | 3. Client sends kind 25910 `initialize` request | ||
| 508 | 4. ESP32 responds with kind 25910 `initialize` result (capabilities, serverInfo) | ||
| 509 | 5. Client sends `notifications/initialized` | ||
| 510 | 6. Client calls `tools/list` or `tools/call` | ||
| 511 | 7. ESP32 dispatches to `mcp_handler.c`, returns result | ||
| 512 | |||
| 513 | #### Encryption | ||
| 514 | |||
| 515 | Phase 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 | ||
| 507 | Only accept commands from owner npub (derived from nsec in config.json). | 534 | Only 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 | |||
| 676 | Uses the standardized ZXing `WIFI:` URI scheme — natively recognized by Android and iOS camera apps: | ||
| 677 | |||
| 678 | ``` | ||
| 679 | WIFI:S:<escaped_ssid>;T:nopass;; | ||
| 680 | ``` | ||
| 681 | |||
| 682 | **Special character escaping**: `;`, `:`, `\`, `,`, `"` are backslash-escaped in the SSID field per spec. | ||
| 683 | |||
| 684 | **Example:** | ||
| 685 | ``` | ||
| 686 | SSID: TollGate-C0E9CA → WIFI:S:TollGate-C0E9CA;T:nopass;; | ||
| 687 | SSID: My;WiFi:Test → WIFI:S:My\;WiFi\:Test;T:nopass;; | ||
| 688 | ``` | ||
| 689 | |||
| 690 | When scanned, the phone **automatically connects** to the TollGate AP — then the captive portal takes over for payment. | ||
| 691 | |||
| 692 | #### QR Cycling | ||
| 693 | |||
| 694 | The display alternates between two QR modes every 5 seconds: | ||
| 695 | 1. **Wi-Fi QR** — `WIFI:S:...;T:nopass;;` — auto-connects phone to AP | ||
| 696 | 2. **Portal URL QR** — `http://10.x.x.1/` — direct link to captive portal | ||
| 697 | |||
| 698 | Label 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 |