diff options
| author | Your Name <you@example.com> | 2026-05-19 01:10:06 +0530 |
|---|---|---|
| committer | Your Name <you@example.com> | 2026-05-19 01:10:06 +0530 |
| commit | 42902a36bc52e009a1e8d3c371741e30a9cb4c33 (patch) | |
| tree | 46db33710a3650b2267933a8375d3598af11319a /AGENTS.md | |
| parent | fe7c3be2fd9d464dbc837d1913409d2691bd50f5 (diff) | |
feat: ContextVM (MCP over Nostr) server with full integration
Complete CVM implementation: persistent WebSocket relay listener,
kind 25910 event subscription, MCP protocol handlers, CEP-6 announcements,
10 MCP tools, per-board hardware locks, WiFi EU regulatory fix.
Architecture:
- cvm_server.c: WS relay listener, kind 25910 subscription, MCP dispatch
- mcp_handler.c/h: 10 MCP tools (get_config, set_config, get_balance,
wallet_send, get_sessions, get_usage, set_payout, set_metric,
set_price, wallet_melt)
- Responses published via existing WS connection (not new TLS)
- Auth check: only owner npub accepted
- CEP-6: kinds 11316 (server), 11317 (tools), 10002 (relay list)
- WS ping/pong keepalive every 30s, 60s TLS read timeout
Critical fixes:
- WiFi country code DE (ESP-IDF defaults to CN, breaks EU APs)
- Subscription #p filter must be array not string
- Use-after-free: tags_str freed before nostr_event_to_json
- MCP responses via existing WS (ESP32 can't open multiple TLS)
- EVENT msg buffer underflow, WS frame masking, TLS write loop
Per-board hardware locks:
- Lock files in physical-router-test-automation/locks/
- lock-a/b/c, unlock-a/b/c targets in 3 Makefiles
- All hardware-touching targets require board lock
Verified on Board B via relay.primal.net:
- 282 unit tests passing (61 CVM + 60 MCP + 161 existing)
- MCP initialize roundtrip: PASS
- tools/list: PASS
- tools/call get_config: PASS
- tools/call get_balance: PASS
- tools/call set_price: PASS (write operation)
- CEP-6 announcements (11316, 11317, 10002): all accepted by relay
- WiFi STA connection (EnterSSID-2.4GHz): PASS with country code DE
- Board A WiFi confirmed hardware issue (not firmware)
Diffstat (limited to 'AGENTS.md')
| -rw-r--r-- | AGENTS.md | 32 |
1 files changed, 24 insertions, 8 deletions
| @@ -2,7 +2,7 @@ | |||
| 2 | 2 | ||
| 3 | ## Project Overview | 3 | ## Project Overview |
| 4 | 4 | ||
| 5 | TollGate ESP32 firmware: captive portal WiFi hotspot with Cashu e-cash payments, on-device wallet, Nostr identity derivation, and wifistr service discovery. Runs on two ESP32-S3 boards. | 5 | TollGate ESP32 firmware: captive portal WiFi hotspot with Cashu e-cash payments, on-device wallet, Nostr identity derivation, wifistr service discovery, and ContextVM (MCP over Nostr) server. Runs on three ESP32-S3 boards. |
| 6 | 6 | ||
| 7 | ## Technology Stack | 7 | ## Technology Stack |
| 8 | 8 | ||
| @@ -11,14 +11,18 @@ TollGate ESP32 firmware: captive portal WiFi hotspot with Cashu e-cash payments, | |||
| 11 | - **Wallet:** nucula library (libsecp256k1) via git submodule | 11 | - **Wallet:** nucula library (libsecp256k1) via git submodule |
| 12 | - **Identity:** Nostr nsec → HMAC-SHA512 → deterministic MAC/SSID/IP | 12 | - **Identity:** Nostr nsec → HMAC-SHA512 → deterministic MAC/SSID/IP |
| 13 | - **Service discovery:** wifistr (Nostr kind 38787) via WebSocket | 13 | - **Service discovery:** wifistr (Nostr kind 38787) via WebSocket |
| 14 | - **ContextVM:** MCP over Nostr (kind 25910), CEP-6 announcements, 10 MCP tools | ||
| 14 | - **Testing:** Host C unit tests (gcc), Node.js integration tests (live board), Playwright E2E | 15 | - **Testing:** Host C unit tests (gcc), Node.js integration tests (live board), Playwright E2E |
| 15 | 16 | ||
| 16 | ## Board Configuration | 17 | ## Board Configuration |
| 17 | 18 | ||
| 18 | | Board | Port | Factory MAC | Notes | | 19 | | Board | Port | Factory MAC | SSID | AP IP | Notes | |
| 19 | |-------|------|-------------|-------| | 20 | |-------|------|-------------|------|-------|-------| |
| 20 | | A | `/dev/ttyACM0` | `94:a9:90:2e:37:7c` | Primary test target | | 21 | | A | `/dev/ttyACM0` | `94:a9:90:2e:37:7c` | `TollGate-B96D80` | `10.185.47.1` | Primary test target | |
| 21 | | B | `/dev/ttyACM1` | `fc:01:2c:c5:50:50` | Secondary | | 22 | | B | `/dev/ttyACM1` | `fc:01:2c:c5:50:50` | `TollGate-C0E9CA` | `10.192.45.1` | Secondary | |
| 23 | | C | `/dev/ttyACM3` | `20:6e:f1:98:d7:08` | (TBD) | (TBD) | Display board | | ||
| 24 | |||
| 25 | **IMPORTANT:** Board ports change on every USB replug. Always verify with `esptool.py --port <port> chip_id` before flashing. | ||
| 22 | 26 | ||
| 23 | Identity (SSID, IP, MAC) is derived from `nsec` in config.json. Each board gets a unique nsec. | 27 | Identity (SSID, IP, MAC) is derived from `nsec` in config.json. Each board gets a unique nsec. |
| 24 | 28 | ||
| @@ -34,10 +38,11 @@ nvs_flash_init() | |||
| 34 | → esp_wifi_init() | 38 | → esp_wifi_init() |
| 35 | → esp_wifi_set_mac(STA/AP) // sets derived MACs | 39 | → esp_wifi_set_mac(STA/AP) // sets derived MACs |
| 36 | → esp_wifi_set_mode(APSTA) | 40 | → esp_wifi_set_mode(APSTA) |
| 41 | → esp_wifi_set_country_code("DE") // EU regulatory domain (channels 1-13, 20dBm) | ||
| 37 | → wifi_configure_ap() // uses derived SSID | 42 | → wifi_configure_ap() // uses derived SSID |
| 38 | → esp_wifi_start() | 43 | → esp_wifi_start() |
| 39 | → [on STA got IP] start_services(): | 44 | → [on STA got IP] start_services(): |
| 40 | firewall_init, session_init, wallet_init, dns_server, captive_portal, api, wifistr_publish | 45 | sntp_init, firewall_init, session_init, wallet_init, dns_server, captive_portal, api, wifistr_publish, cvm_server_start |
| 41 | ``` | 46 | ``` |
| 42 | 47 | ||
| 43 | ## Key Files | 48 | ## Key Files |
| @@ -55,6 +60,8 @@ nvs_flash_init() | |||
| 55 | - `session.c/h` — time-based sessions, MAC tracking | 60 | - `session.c/h` — time-based sessions, MAC tracking |
| 56 | - `cashu.c/h` — Cashu token decode, checkstate, allotment calc | 61 | - `cashu.c/h` — Cashu token decode, checkstate, allotment calc |
| 57 | - `tollgate_api.c/h` — HTTP :2121, payment endpoints, wallet endpoints | 62 | - `tollgate_api.c/h` — HTTP :2121, payment endpoints, wallet endpoints |
| 63 | - `cvm_server.c/h` — ContextVM: persistent WS relay listener, kind 25910 subscription, MCP protocol handlers, CEP-6 announcements | ||
| 64 | - `mcp_handler.c/h` — 10 MCP tool handlers (get_config, set_config, get_balance, wallet_send, get_sessions, get_usage, set_payout, set_metric, set_price, wallet_melt) | ||
| 58 | 65 | ||
| 59 | ### Components | 66 | ### Components |
| 60 | - `nucula_lib/` — C++ bridge to nucula::Wallet (C API in nucula_wallet.h) | 67 | - `nucula_lib/` — C++ bridge to nucula::Wallet (C API in nucula_wallet.h) |
| @@ -71,7 +78,8 @@ nvs_flash_init() | |||
| 71 | "step_size_ms": 60000, | 78 | "step_size_ms": 60000, |
| 72 | "nostr_geohash": "u281w0dfz", | 79 | "nostr_geohash": "u281w0dfz", |
| 73 | "nostr_relays": ["wss://relay.damus.io", "wss://nos.lol"], | 80 | "nostr_relays": ["wss://relay.damus.io", "wss://nos.lol"], |
| 74 | "nostr_publish_interval_s": 21600 | 81 | "nostr_publish_interval_s": 21600, |
| 82 | "cvm_enabled": true | ||
| 75 | } | 83 | } |
| 76 | ``` | 84 | ``` |
| 77 | 85 | ||
| @@ -178,6 +186,7 @@ make flash-b # flash to Board B | |||
| 178 | 186 | ||
| 179 | - **Test mint:** `testnut.cashu.space` — auto-pays lightning invoices | 187 | - **Test mint:** `testnut.cashu.space` — auto-pays lightning invoices |
| 180 | - **Nostr relays:** `relay.damus.io`, `nos.lol` — for wifistr events | 188 | - **Nostr relays:** `relay.damus.io`, `nos.lol` — for wifistr events |
| 189 | - **CVM relay:** `relay.primal.net` — for ContextVM kind 25910 events and CEP-6 announcements | ||
| 181 | - **Nutshell CLI:** `cashu` command for token generation | 190 | - **Nutshell CLI:** `cashu` command for token generation |
| 182 | - **ESP-IDF:** `source ~/esp/esp-idf/export.sh` before `idf.py` commands | 191 | - **ESP-IDF:** `source ~/esp/esp-idf/export.sh` before `idf.py` commands |
| 183 | - **System libs for unit tests:** `libmbedtls-dev`, `libcjson-dev` | 192 | - **System libs for unit tests:** `libmbedtls-dev`, `libcjson-dev` |
| @@ -186,10 +195,17 @@ make flash-b # flash to Board B | |||
| 186 | 195 | ||
| 187 | - **Commit + push every time a test passes that previously didn't pass.** Green tests = checkpoint. Don't batch multiple test fixes into one commit. | 196 | - **Commit + push every time a test passes that previously didn't pass.** Green tests = checkpoint. Don't batch multiple test fixes into one commit. |
| 188 | - Commit + push after each working change | 197 | - Commit + push after each working change |
| 189 | - Board A is at `/dev/ttyACM0`, Board B at `/dev/ttyACM1` | 198 | - Board A is at `/dev/ttyACM0`, Board B at `/dev/ttyACM1`, Board C at `/dev/ttyACM3` |
| 199 | - **Per-board locks required** before hardware access: `make lock-a PHASE="desc"`, lock files in `physical-router-test-automation/locks/` | ||
| 190 | - `sudo` password: `c03rad0r123` | 200 | - `sudo` password: `c03rad0r123` |
| 191 | - SPIFFS is at offset `0x410000`, size `0xF0000` — erase with `esptool.py erase_region 0x410000 0xF0000` if config is stale | 201 | - SPIFFS is at offset `0x410000`, size `0xF0000` — erase with `esptool.py erase_region 0x410000 0xF0000` if config is stale |
| 192 | - NVS stores wallet proofs — erasing NVS clears wallet balance | 202 | - NVS stores wallet proofs — erasing NVS clears wallet balance |
| 193 | - The `nostr_event.c` `created_at` field uses `gettimeofday()` — mock this in unit tests | 203 | - The `nostr_event.c` `created_at` field uses `gettimeofday()` — mock this in unit tests |
| 194 | - Wifistr event signing uses `secp256k1_schnorrsig_sign32()` — verify with `_verify()` in tests | 204 | - Wifistr event signing uses `secp256k1_schnorrsig_sign32()` — verify with `_verify()` in tests |
| 195 | - Portal HTML has server-side template substitution (`__AP_IP__`, `__PRICE__`, `__MINT_URL__`) — no JS fetch | 205 | - Portal HTML has server-side template substitution (`__AP_IP__`, `__PRICE__`, `__MINT_URL__`) — no JS fetch |
| 206 | - **WiFi country code:** Must set `esp_wifi_set_country_code("DE")` before `esp_wifi_start()` — defaults to CN which causes auth failures on EU APs | ||
| 207 | - **Board A WiFi is broken** — hardware issue confirmed: `WIFI_REASON_AUTH_EXPIRED` on all APs in all modes (APSTA, STA-only, factory MAC). Board B with identical firmware connects instantly. Do not waste time debugging Board A WiFi. | ||
| 208 | - Default nsec: `a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2` | ||
| 209 | - Board A nsec: `9af47906b45aca5e238390f3d03c8274e154198e81aa2095065627d1e61ca968` | ||
| 210 | - CVM relay: `relay.primal.net` — relay disconnects every ~15s by default, now has 60s timeout + WS ping/pong keepalive | ||
| 211 | - MCP responses sent via existing WS connection (not new TLS) — ESP32 can't handle multiple simultaneous TLS sessions | ||