Unified Agent + reusable Go agent core.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Improve RPC error handling with ID extraction

Lyric 848c54bd e6d5cbea

+699
+1
README.md
··· 23 23 - 🤝 **Mesh Agent Exchange Protocol (MAEP)**: You and your amigos run multiple agents and want them to message each other: use the MAEP, a p2p protocol with trust-state and audit trails. 24 24 - 🔒 **Serious secure defaults**: Profile-based credential injection, Guard redaction, outbound policy controls, and async approvals with audit trails (see [docs/security.md](docs/security.md)). 25 25 - 🧰 **Practical Skills system**: Discover + inject `SKILL.md` from `~/.morph`, `~/.claude`, and `~/.codex`, with smart routing plus explicit control (see [docs/skills.md](docs/skills.md)). 26 + - 📚 **Beginner-friendly**: Built as a learning-first agent project, with detailed design docs in `docs/` and practical debugging tools like `--inspect-prompt` and `--inspect-request`. 26 27 27 28 ## Quickstart 28 29
+465
docs/feat/feat_20260206_maep.md
··· 1 + --- 2 + date: 2026-02-06 3 + title: Mesh Agent Exchange Protocol (v1) 4 + status: draft 5 + --- 6 + 7 + # Mesh Agent Exchange Protocol (MAEP) v1 8 + 9 + ## 1) Summary 10 + v1 targets a minimum viable end-to-end loop: 11 + - Verifiable node identity (based on libp2p `peer_id`). 12 + - Local contacts management (no online directory service). 13 + - libp2p peer-to-peer communication (direct first, relay fallback). 14 + - JSON-RPC data exchange (minimal method set in v1). 15 + - Encrypted transport sessions (relay cannot decrypt business payloads). 16 + 17 + Notes: 18 + - v1 1v1 E2EE depends on libp2p secure channels. 19 + - v1 does not include asynchronous session bootstrapping via X3DH / Double Ratchet, nor per-message ratcheting. 20 + 21 + ## 2) Goals 22 + - Allow two agents to complete secure online data exchange after exchanging contact cards. 23 + - Automatically fall back to relay when direct connection fails. 24 + - Ensure `node_uuid` is never treated as a standalone identity credential. 25 + - Keep implementation behavior consistent and avoid cross-language interoperability drift. 26 + 27 + ## 3) Non-Goals 28 + - No online directory service. 29 + - No group E2EE. 30 + - No durable retransmission queue. 31 + - No exactly-once guarantee. 32 + - No x402 billing flow (deferred to v2). 33 + 34 + ## 4) Terminology 35 + - `peer_id`: libp2p Peer ID (protocol-layer identity used for auth and connection matching). 36 + - `node_id`: business namespace identifier, defined as `maep:<peer_id>`, used only for display/logging/cross-system references. 37 + - `node_uuid`: business alias (UUIDv7), used only for contact lookup and display. 38 + - `identity_keypair`: local Ed25519 identity keypair. 39 + - `identity_pub_ed25519`: externally shared Ed25519 public key. 40 + 41 + Normative requirements: 42 + - Any identity-equality check must compare `peer_id`, never `node_uuid`. 43 + - `peer_id` must be parsed/derived via libp2p SDK APIs; custom hash implementations are forbidden. 44 + 45 + ## 5) Identity And Contact Card 46 + 47 + ### 5.1 Identity Fields 48 + - `node_uuid` 49 + - `peer_id` 50 + - `node_id` (optional derived value: `maep:<peer_id>`) 51 + - `identity_pub_ed25519` 52 + 53 + Implementation requirement: 54 + - The local libp2p host must use the same `identity_keypair` as its host identity key. 55 + - Otherwise, runtime `RemotePeer()` may diverge from the contact card `peer_id`, causing interop failures. 56 + 57 + ### 5.2 Contact Card Schema 58 + Recommended fields: 59 + - `version` 60 + - `node_uuid` 61 + - `peer_id` 62 + - `node_id` 63 + - `identity_pub_ed25519` 64 + - `addresses` 65 + - `min_supported_protocol` 66 + - `max_supported_protocol` 67 + - `issued_at` 68 + - `expires_at` (recommended) 69 + - `key_rotation_of` (optional) 70 + 71 + Encoding rules: 72 + - `identity_pub_ed25519`: base64url (no padding) encoding of Ed25519 public key bytes. 73 + - `sig`: base64url (no padding) encoding of detached signature bytes. 74 + - `addresses`: multiaddr; each address must end with `/p2p/<peer_id>`. 75 + Note: relay segments are allowed in the middle (for example `/p2p/<relay_peer_id>/p2p-circuit/...`). 76 + 77 + JSON/JCS constraints (hardcoded for MVP): 78 + - `null` is not allowed. 79 + - Floating-point numbers are not allowed (only integer numeric type). 80 + - Duplicate keys are not allowed. 81 + - In JSON-RPC request/response parsing paths, any violation above returns `ERR_INVALID_JSON_PROFILE`. 82 + - In contact-card import paths, payload/structure violations are unified as `ERR_INVALID_CONTACT_CARD` (more granular causes may still be logged). 83 + - `identity_pub_ed25519` must decode via base64url to exactly 32 bytes (Ed25519 raw public key). 84 + - `DerivePeerID`, fingerprinting, and signature verification must always use decoded raw bytes as input. 85 + 86 + JCS example (`sig` excluded from signed object): 87 + ```json 88 + { 89 + "payload": { 90 + "version": 1, 91 + "node_uuid": "0194f5c0-8f6e-7d9d-a4d7-6d8d4f35f456", 92 + "peer_id": "12D3KooWabc", 93 + "node_id": "maep:12D3KooWabc", 94 + "identity_pub_ed25519": "cHVibGljLWtleS1ieXRlcy1iYXNlNjR1cmw", 95 + "addresses": ["/ip4/203.0.113.8/udp/4001/quic-v1/p2p/12D3KooWabc"], 96 + "min_supported_protocol": 1, 97 + "max_supported_protocol": 1, 98 + "issued_at": "2026-02-06T16:00:00Z", 99 + "expires_at": "2026-08-06T16:00:00Z" 100 + }, 101 + "sig_alg": "ed25519", 102 + "sig_format": "jcs-rfc8785-detached", 103 + "sig": "base64url_signature" 104 + } 105 + ``` 106 + 107 + The `peer_id` above is intentionally truncated for readability; real payloads must use the full peer-id string. 108 + 109 + Recommended signing input: 110 + - `"maep-contact-card-v1\n" + canonical_json_bytes(payload)` 111 + 112 + ### 5.3 Import-Time Validation (Offline) 113 + Contact-card import must cover all checks below (implementation order may vary): 114 + 1. Validate `expires_at` (if present and already expired, reject import). 115 + 2. Compute `pub = base64url_decode(identity_pub_ed25519)`; it must be 32 bytes, otherwise `ERR_INVALID_CONTACT_CARD`. 116 + 3. Build a public key object from `pub` via libp2p SDK and derive `derived_peer_id` via SDK APIs (custom implementation forbidden). 117 + 4. Parse payload `peer_id` as `expected_peer_id`; compare binary values of `derived_peer_id` and `expected_peer_id`. 118 + 5. Validate `node_id == "maep:" + peer_id` (when `node_id` is provided). 119 + 6. Validate `sig` (JCS canonical payload + domain separator). 120 + 7. Parse each `addresses` entry and verify the terminal peer component equals payload `peer_id`. 121 + 8. If local state already has the same `node_uuid` mapped to a different `peer_id`, mark as `conflicted` and forbid automatic overwrite. 122 + 123 + ### 5.4 Connect-Time Validation (Online) 124 + Connection-time validation must perform: 125 + 1. Select `expected_peer_id` from the contact. 126 + 2. Dial only addresses containing `/p2p/<expected_peer_id>`; reject others directly. 127 + 3. After connection establishment, read `RemotePeer()` and `RemotePublicKey()` from the libp2p connection. 128 + 4. Validate `RemotePeer() == expected_peer_id`. 129 + 5. On mismatch, return `ERR_PEER_ID_MISMATCH`, disconnect immediately, and emit a high-priority security alert. 130 + 131 + ## 6) Transport And Connectivity 132 + 133 + ### 6.1 Transport Stack 134 + - The current implementation uses libp2p default secure transport behavior (QUIC/TCP chosen by reachability and negotiation). 135 + - Relay uses Circuit Relay v2. 136 + - Explicit enforced QUIC-priority scheduling is not implemented yet (can be a future optimization). 137 + 138 + ### 6.2 Dial Priority 139 + Connection order (current implementation): 140 + 1. Try direct addresses first (without `/p2p-circuit`), in input order. 141 + 2. Then try relay addresses (with `/p2p-circuit`), in input order. 142 + 3. If caller provides explicit address list, use it; otherwise use contact-saved `addresses`. 143 + 4. Automatic reordering by `last_ok_at` is not implemented yet. 144 + 145 + ### 6.3 Direct-Failure Classification 146 + The current implementation does not emit structured failure classification codes; dial failures are returned as aggregated per-address error strings. 147 + Actual behavior: 148 + - Retry by address order: direct first, then relay. 149 + - Per-address dial timeout defaults to 3 seconds (`DialAddrTimeout`). 150 + - Identity mismatch (`peer_id_mismatch`) disconnects immediately during stream validation and returns `ERR_PEER_ID_MISMATCH`. 151 + - A total direct-attempt budget of 8-12 seconds is not implemented yet. 152 + 153 + ### 6.4 Default Limits (MVP fixed) 154 + - `max_rpc_request_bytes = 256 KiB` 155 + - `max_payload_bytes = 128 KiB` (decoded bytes of `payload_base64`) 156 + - `hello_timeout = 3s` 157 + - `rpc_timeout_default = 10s` 158 + - `rate_limit_data_push_per_peer = 120/min` 159 + 160 + ## 7) RPC Model 161 + 162 + ### 7.1 Protocol IDs 163 + - `hello`: `/maep/hello/1.0.0` 164 + - `rpc`: `/maep/rpc/1.0.0` 165 + 166 + ### 7.2 Stream Framing 167 + v1 is fixed to one-stream-per-request: 168 + 1. Client opens `/maep/rpc/1.0.0` stream. 169 + 2. Client writes full JSON-RPC request. 170 + 3. Client half-closes write side. 171 + 4. Server returns one JSON-RPC response. 172 + 5. Stream closes. 173 + 174 + Note: 175 + - v1 does not support multiplexed framing. 176 + 177 + ### 7.3 Version Negotiation (`hello`) 178 + Both sides exchange on `/maep/hello/1.0.0`: 179 + - `protocol_min` 180 + - `protocol_max` 181 + - `capabilities` 182 + 183 + `hello` stream semantics (fixed in v1): 184 + 1. Dialer opens `/maep/hello/1.0.0` stream, writes one UTF-8 JSON `hello` object, then half-closes. 185 + 2. Listener reads request, writes one UTF-8 JSON `hello` object response, and closes stream. 186 + 3. Each side computes `negotiated_protocol`; mismatch indicates implementation error and should trigger disconnect alerting. 187 + 4. `/maep/rpc/1.0.0` requests must not be processed before successful `hello` negotiation. 188 + 189 + Negotiation rules: 190 + - `negotiated = min(local_max, remote_max)` 191 + - Must satisfy `negotiated >= max(local_min, remote_min)` 192 + - If no overlap, return `ERR_UNSUPPORTED_PROTOCOL` and disconnect 193 + 194 + Example: 195 + ```json 196 + { 197 + "type": "hello", 198 + "protocol_min": 1, 199 + "protocol_max": 1, 200 + "capabilities": ["rpc.data.push.v1"] 201 + } 202 + ``` 203 + 204 + ### 7.4 Allowed Methods 205 + Default allowlist: 206 + - `agent.ping` 207 + - `agent.capabilities.get` 208 + - `agent.data.push` 209 + 210 + Methods outside the allowlist must be rejected with `ERR_METHOD_NOT_ALLOWED`. 211 + 212 + ### 7.5 `agent.data.push` Semantics 213 + `agent.data.push` is used for data exchange, including chat messages. 214 + 215 + Request example: 216 + ```json 217 + { 218 + "jsonrpc": "2.0", 219 + "id": "req-7f9f", 220 + "method": "agent.data.push", 221 + "params": { 222 + "topic": "chat.message", 223 + "content_type": "application/json", 224 + "payload_base64": "eyJtZXNzYWdlX2lkIjoibXNnXzAwMSIsInRleHQiOiLllYgiLCJzZW50X2F0IjoiMjAyNi0wMi0wNlQxNjozMDowMFoiLCJzZXNzaW9uX2lkIjoiMDE5NGY1YzAtOGY2ZS03ZDlkLWE0ZDctNmQ4ZDRmMzVmNDU2In0", 225 + "idempotency_key": "m-001" 226 + } 227 + } 228 + ``` 229 + 230 + Notification and request rules: 231 + - Without `id`: notification, no response. 232 + - With `id`: return acceptance-state response, not a durability guarantee. 233 + 234 + JSON-RPC constraints (hardcoded for MVP): 235 + - `null` is not allowed in request/response JSON body. 236 + - Floating-point numbers are not allowed in protocol fields (integer numeric type only). 237 + - Violations return `ERR_INVALID_JSON_PROFILE`. 238 + - `agent.data.push.params.content_type` must start with `application/json` (for example `application/json`, `application/json; charset=utf-8`); `text/plain` is rejected at protocol layer. 239 + - `payload_base64` must be base64url without padding; decode failure returns `ERR_INVALID_PARAMS`. 240 + - Decoded `payload_base64` must be an envelope JSON object and include at least: 241 + - `message_id` (non-empty string) 242 + - `text` (non-empty string) 243 + - `sent_at` (RFC3339 string) 244 + - Dialogue topics (`share.proactive.v1` / `dm.checkin.v1` / `dm.reply.v1` / `chat.message`) must include `session_id`, and `session_id` must be UUIDv7. 245 + - If decoded `payload_base64` exceeds `max_payload_bytes`, return `ERR_PAYLOAD_TOO_LARGE`. 246 + - No protocol-level fallback: no auto conversion from plain text to envelope, and no auto-generated `session_id`. Invalid payloads are rejected directly (`ERR_INVALID_PARAMS`). 247 + 248 + ### 7.6 Business Metadata Topic (`profile.intro.v1`, planned) 249 + Implementation status: 250 + - Current code does not implement dedicated handling for `profile.intro.v1`. 251 + - Current code does not persist `remote_public_nickname` (or equivalent field). 252 + - If this topic is sent now, it is handled as ordinary `agent.data.push` envelope input. 253 + 254 + Future design constraints: 255 + Purpose: 256 + - Exchange public self-profile data at business layer, such as outward-facing nickname. 257 + - Must not participate in protocol-level auth or alter `peer_id/node_id/trust_state`. 258 + 259 + Constraints: 260 + - Still use `agent.data.push`. 261 + - `content_type` must start with `application/json`. 262 + - Decoded `payload_base64` must be envelope JSON and satisfy required fields from 7.5. 263 + 264 + Suggested envelope example: 265 + ```json 266 + { 267 + "message_id": "msg_019db8f8-7e6f-79f5-8e92-d5f4f8fd0d74", 268 + "text": "profile intro update", 269 + "sent_at": "2026-02-07T12:00:00Z", 270 + "profile": { 271 + "public_nickname": "Mochi", 272 + "lang": "zh-CN", 273 + "updated_at": "2026-02-07T12:00:00Z" 274 + } 275 + } 276 + ``` 277 + 278 + Receiver-side handling recommendations: 279 + - If `profile.public_nickname` is empty, ignore update. 280 + - If `profile.updated_at` is not RFC3339, ignore that profile update. 281 + - Business layer should store peer-declared nickname as `remote_public_nickname` (or equivalent), and should not forcibly overwrite local `contact_nickname`. 282 + - If local nickname overwrite is needed, it must go through explicit business policy and emit an audit log. 283 + 284 + Response example (acceptance state): 285 + ```json 286 + { 287 + "jsonrpc": "2.0", 288 + "id": "req-7f9f", 289 + "result": { 290 + "accepted": true, 291 + "deduped": false 292 + } 293 + } 294 + ``` 295 + 296 + ## 8) Reliability And Idempotency 297 + - Online semantics: best-effort + request timeout. 298 + - Idempotency dedupe key: tuple `(from_peer_id, topic, idempotency_key)`. 299 + - First receipt: process and record dedupe key. 300 + - Duplicate receipt: do not process again; return `deduped=true` (when `id` exists) or silently drop (notification). 301 + - Dedupe record TTL: default 7 days (configurable). 302 + - Dedupe table cap: default global 10k records; evict oldest first when exceeded. 303 + 304 + ## 9) Security And Trust State 305 + 306 + ### 9.1 Relay Visibility 307 + Visible to relay: 308 + - Connection relationships, traffic size, timing, and online status. 309 + 310 + Not visible to relay: 311 + - RPC business plaintext payload. 312 + 313 + ### 9.2 Trust States 314 + - `tofu`: cryptographic checks pass, but no second-channel verification yet. 315 + - `verified`: second-channel fingerprint verification completed. 316 + - `conflicted`: `node_uuid -> peer_id` conflict or critical identity mismatch. 317 + - `revoked`: revoked; communication forbidden. 318 + 319 + Rules: 320 + - `tofu` is not `verified`. 321 + - `revoked` and `conflicted` must block business communication. 322 + 323 + ### 9.3 Fingerprint Verification Flow 324 + Fingerprint definition: 325 + - `fingerprint = SHA256(base64url_decode(identity_pub_ed25519))` (displayed in grouped hex). 326 + 327 + Recommended flow: 328 + 1. Set state to `tofu` after import. 329 + 2. Show short fingerprint (for example 8 groups of 4 hex chars). 330 + 3. Compare via second channel (face-to-face QR, spoken grouped code, already-verified chat channel). 331 + 4. Promote to `verified` only on full match. 332 + 5. If mismatch, set `conflicted` and alert. 333 + 334 + Security note: 335 + - Without second-channel verification, first-contact impersonation risk remains. 336 + 337 + ## 10) Error Codes And Handling 338 + ### 10.1 JSON-RPC Error Object 339 + RPC errors must use standard JSON-RPC structure: 340 + ```json 341 + { 342 + "jsonrpc": "2.0", 343 + "id": "req-7f9f", 344 + "error": { 345 + "code": -32004, 346 + "message": "ERR_METHOD_NOT_ALLOWED", 347 + "data": { "details": "method=agent.foo" } 348 + } 349 + } 350 + ``` 351 + 352 + Rules: 353 + - `error.code` must be an integer. 354 + - `error.message` must use `ERR_*` symbols from the table below. 355 + - Notifications (without `id`) must not return any response (including errors); only log/metrics, and disconnect if needed. 356 + - If request parsing fails and a valid `id` is extractable, return corresponding `ERR_*` error object; otherwise log only and do not respond. 357 + 358 + ### 10.2 Symbol To JSON-RPC Code Mapping (MVP fixed) 359 + | Symbol | JSON-RPC `error.code` | 360 + |---|---:| 361 + | `ERR_UNAUTHORIZED` | -32001 | 362 + | `ERR_PEER_ID_MISMATCH` | -32002 | 363 + | `ERR_CONTACT_CONFLICTED` | -32003 | 364 + | `ERR_METHOD_NOT_ALLOWED` | -32004 | 365 + | `ERR_PAYLOAD_TOO_LARGE` | -32005 | 366 + | `ERR_RATE_LIMITED` | -32006 | 367 + | `ERR_UNSUPPORTED_PROTOCOL` | -32007 | 368 + | `ERR_INVALID_JSON_PROFILE` | -32008 | 369 + | `ERR_INVALID_CONTACT_CARD` | -32009 | 370 + | `ERR_INVALID_PARAMS` | -32602 | 371 + 372 + ### 10.3 Connection/Request Handling 373 + | Symbol | Trigger | Connection Action | Request Action | 374 + |---|---|---|---| 375 + | `ERR_UNAUTHORIZED` | Remote not in contacts, or `trust_state` is `revoked/conflicted` | disconnect | reject | 376 + | `ERR_PEER_ID_MISMATCH` | `RemotePeer() != expected_peer_id` | disconnect | reject | 377 + | `ERR_CONTACT_CONFLICTED` | Conflict found during contact-card import (`node_uuid` mapped to different `peer_id`) | n/a | reject(import) | 378 + | `ERR_METHOD_NOT_ALLOWED` | Method not in allowlist | keep | reject | 379 + | `ERR_PAYLOAD_TOO_LARGE` | Exceeds payload limit | keep | reject | 380 + | `ERR_RATE_LIMITED` | Rate limit exceeded | keep | reject | 381 + | `ERR_INVALID_JSON_PROFILE` | `null`/floating-point/duplicate key detected | keep | reject | 382 + | `ERR_INVALID_CONTACT_CARD` | Invalid contact-card fields (import path) | n/a | reject(import) | 383 + | `ERR_INVALID_PARAMS` | Invalid parameter format (for example `payload_base64` decode failure) | keep | reject | 384 + | `ERR_UNSUPPORTED_PROTOCOL` | No protocol-version overlap | disconnect | reject | 385 + 386 + ## 11) Protocol Upgrade And Downgrade 387 + 388 + ### 11.1 Compatibility Rules 389 + - `v1.x` must remain backward compatible. 390 + - New fields can only be added as optional. 391 + - Unknown fields must be ignored, not rejected. 392 + - Breaking changes require major version bump (`v2`). 393 + 394 + ### 11.2 Rollout Order 395 + Recommended order: 396 + 1. Upgrade relay first. 397 + 2. Upgrade passive receivers next. 398 + 3. Upgrade active initiators last. 399 + 400 + ### 11.3 Downgrade Detection 401 + Nodes should record: 402 + - `last_remote_max_protocol` 403 + - `last_negotiated_protocol` 404 + 405 + Trigger `downgrade_suspected` when either condition is met: 406 + - Current `remote_max_protocol` is lower than historical value. 407 + - Current `negotiated_protocol` is lower than historical value. 408 + 409 + Current implementation note: 410 + - Current implementation does not gate on network-path changes (for example relay/address-family changes); any version decrease triggers alerting. 411 + 412 + Default handling: 413 + - Log high-priority warning and prompt manual verification. 414 + - Keep connection usable. 415 + 416 + Optional strict mode: 417 + - Allow only `agent.ping` and `agent.capabilities.get`; temporarily disable `agent.data.push`. 418 + 419 + ## 12) Storage Model 420 + MVP requirements: 421 + - Provide storage abstraction interfaces first (identity / contacts / audit / dedupe / protocol history). 422 + - Default backend uses local files (no database dependency). 423 + - Future SQLite/other backends can be added, but must not change upper-layer protocol behavior. 424 + 425 + Recommended file-backend partitions (logically equivalent to tables): 426 + - `node_identity` 427 + - `node_uuid`, `peer_id`, `node_id`, `identity_pub_ed25519`, `identity_priv_ed25519`, `created_at` 428 + - `contacts` 429 + - `node_uuid`, `peer_id`, `node_id`, `display_name`, `identity_pub_ed25519`, `addresses`, `trust_state`, `last_seen` 430 + - `audit_events` 431 + - `event_id`, `action`, `peer_id`, `node_uuid`, `previous_trust_state`, `new_trust_state`, `reason`, `metadata`, `created_at` 432 + - `dedupe_records` 433 + - `from_peer_id`, `topic`, `idempotency_key`, `created_at`, `expires_at` 434 + - `protocol_history` 435 + - `peer_id`, `last_remote_max_protocol`, `last_negotiated_protocol`, `updated_at` 436 + 437 + Implementation recommendations: 438 + - Use atomic file replacement on writes (temp file + rename). 439 + - Recommended permissions: directory `0700`, state files `0600`. 440 + 441 + ## 13) Rollout Plan 442 + ### Phase 0: Spec Freeze 443 + - [x] Freeze `peer_id` definition and contact-card fields. 444 + - [x] Freeze protocol IDs (`/maep/hello/1.0.0`, `/maep/rpc/1.0.0`). 445 + - [x] Freeze error codes and disconnect strategy. 446 + - [x] Freeze JCS signing input and default `allowed_methods` allowlist. 447 + 448 + ### Phase 1: MVP 449 + - [x] Node identity generation and persistence (`node_uuid/peer_id/node_id`). 450 + - [x] contacts import/export (out-of-band). 451 + - [x] Direct-first + relay-fallback dialing. 452 + - [x] hello negotiation + JSON-RPC one-request-per-stream. 453 + - [x] `agent.ping` / `agent.capabilities.get` / `agent.data.push`. 454 + - [x] trust_state and conflict-mapping enforcement. 455 + - [x] dedupe TTL and capacity eviction. 456 + 457 + ## 14) Open Questions 458 + - Should the default relay policy be `verified_only=false + rate limiting for tofu`, or `verified_only=true`? 459 + 460 + ## References 461 + - Peer IDs: https://github.com/libp2p/specs/blob/master/peer-ids/peer-ids.md 462 + - Secure Channels: https://docs.libp2p.io/concepts/secure-comm/overview/ 463 + - Circuit Relay v2: https://docs.libp2p.io/concepts/nat/circuit-relay/ 464 + - go-libp2p network API: https://pkg.go.dev/github.com/libp2p/go-libp2p/core/network 465 + - JSON-RPC 2.0: https://www.jsonrpc.org/specification
+141
docs/feat/feat_20260206_maep_impl.md
··· 1 + --- 2 + date: 2026-02-06 3 + title: MAEP v1 Implementation Notes (File Store First) 4 + status: draft 5 + --- 6 + 7 + # MAEP v1 Implementation Details (In Progress) 8 + 9 + ## 1) Current Scope 10 + Without introducing a database, this phase delivers MAEP v1 core infrastructure first: 11 + - Identity generation and persistence. 12 + - JCS signing and verification for contact cards. 13 + - Contact import validation (`peer_id` / `node_id` / terminal `/p2p/<peer_id>` in multiaddr). 14 + - Basic trust-state transitions (`tofu -> verified`, conflict marked as `conflicted`). 15 + - `/maep/hello/1.0.0` and `/maep/rpc/1.0.0` (one stream per request). 16 + - Minimal RPC method set: `agent.ping` / `agent.capabilities.get` / `agent.data.push`. 17 + - File backend for `dedupe_records` / `protocol_history` / `inbox_messages` / `outbox_messages` / `audit_events`. 18 + - CLI management entry points. 19 + 20 + ## 2) Storage Abstraction 21 + Code location: `maep/store.go` 22 + 23 + Interfaces: 24 + - `Ensure(ctx)` 25 + - `GetIdentity(ctx)` / `PutIdentity(ctx, identity)` 26 + - `GetContactByPeerID(ctx, peerID)` 27 + - `GetContactByNodeUUID(ctx, nodeUUID)` 28 + - `PutContact(ctx, contact)` 29 + - `ListContacts(ctx)` 30 + - `AppendAuditEvent(ctx, event)` 31 + - `ListAuditEvents(ctx, peerID, action, limit)` 32 + - `AppendInboxMessage(ctx, message)` 33 + - `ListInboxMessages(ctx, fromPeerID, topic, limit)` 34 + - `AppendOutboxMessage(ctx, message)` 35 + - `ListOutboxMessages(ctx, toPeerID, topic, limit)` 36 + - `GetDedupeRecord(ctx, fromPeerID, topic, idempotencyKey)` 37 + - `PutDedupeRecord(ctx, record)` 38 + - `PruneDedupeRecords(ctx, now, maxEntries)` 39 + - `GetProtocolHistory(ctx, peerID)` 40 + - `PutProtocolHistory(ctx, history)` 41 + 42 + Design intent: 43 + - Protocol logic (`maep/service.go`) depends only on interfaces. 44 + - Adding SQLite/Badger/remote store later must not change business validation paths. 45 + 46 + ## 3) File Backend 47 + Code location: `maep/file_store.go` 48 + 49 + Directory: defaults to `file_state_dir/maep` (override via CLI `--dir`). 50 + 51 + Files: 52 + - `identity.json` 53 + - `contacts.json` 54 + - `audit_events.jsonl` 55 + - `inbox_messages.jsonl` 56 + - `outbox_messages.jsonl` 57 + - `dedupe_records.json` 58 + - `protocol_history.json` 59 + 60 + Implementation notes: 61 + - File permission `0600`, directory permission `0700`. 62 + - Writes use atomic replacement (temp file + rename) to reduce corruption risk. 63 + - `contacts.json` uses a fixed `version` field for future migrations. 64 + - `inbox/outbox` use a unified envelope shape: `message_id/topic/content_type/payload_base64/idempotency_key/session_id/reply_to + timestamp` (`received_at` for inbox, `sent_at` for outbox). 65 + 66 + ## 4) Identity And Contact Card 67 + Code locations: 68 + - `maep/identity.go` 69 + - `maep/contact_card.go` 70 + - `maep/jsonprofile.go` 71 + 72 + Implemented rules: 73 + - Identity keys: Ed25519. 74 + - `peer_id`: derived from public key via libp2p SDK (custom hash forbidden). 75 + - `node_id = "maep:" + peer_id`. 76 + - `identity_pub_ed25519`: base64url (no padding), must decode to 32 bytes. 77 + - JCS: RFC8785 canonicalization (`jsoncanonicalizer.Transform`). 78 + - Signing input: `"maep-contact-card-v1\n" + canonical_payload`. 79 + - Strict JSON profile: reject `null`, floating-point, and duplicate keys. 80 + - Error-symbol conventions: JSON-RPC parsing violations map to `ERR_INVALID_JSON_PROFILE`; contact-card import structural/semantic violations map to `ERR_INVALID_CONTACT_CARD`. 81 + 82 + ## 5) CLI Entry Points 83 + Code location: `cmd/mistermorph/maepcmd/maep.go` 84 + 85 + Commands: 86 + - `mistermorph maep init` 87 + - `mistermorph maep id` 88 + - `mistermorph maep card export --address ...` 89 + - `mistermorph maep contacts list` 90 + - `mistermorph maep contacts import <contact_card.json|->` 91 + - `mistermorph maep contacts show <peer_id>` 92 + - `mistermorph maep contacts verify <peer_id>` 93 + - `mistermorph maep audit list --limit 100` 94 + - `mistermorph maep inbox list --limit 50` 95 + - `mistermorph maep outbox list --limit 50` 96 + - `mistermorph maep serve` 97 + - `mistermorph maep hello <peer_id>` 98 + - `mistermorph maep ping <peer_id>` 99 + - `mistermorph maep capabilities <peer_id>` 100 + - `mistermorph maep push <peer_id> --text ...` 101 + 102 + ## 6) Completed / Next 103 + Completed: 104 + - [x] Store abstraction interface 105 + - [x] File store 106 + - [x] Identity generation and persistence 107 + - [x] Contact-card JCS signing and verification 108 + - [x] Contact import validation and conflict marking 109 + - [x] hello negotiation (`/maep/hello/1.0.0`) 110 + - [x] RPC handling (`/maep/rpc/1.0.0`, one request per stream) 111 + - [x] `agent.ping` / `agent.capabilities.get` / `agent.data.push` 112 + - [x] Dedupe file backend (`dedupe_records.json`) 113 + - [x] Protocol-history file backend (`protocol_history.json`) 114 + - [x] Inbox file backend (`inbox_messages.jsonl`) 115 + - [x] Outbox file backend (`outbox_messages.jsonl`) 116 + - [x] Actual `ERR_RATE_LIMITED` enforcement (per peer per minute) 117 + - [x] Local inbox query CLI for `agent.data.push` (`maep inbox list`) 118 + - [x] Local outbox query CLI for `agent.data.push` (`maep outbox list`) 119 + - [x] Dial priority: direct first, relay second (classified by `/p2p-circuit`) 120 + - [x] Audit logs for trust-state/contact operations (`audit_events.jsonl` + `maep audit list`) 121 + - [x] Network and CLI baseline commands 122 + - [x] On RPC parse failure, reply with `ERR_*` if a valid `id` is best-effort extractable; log-only with no response if `id` cannot be extracted 123 + 124 + Next phase: 125 + - [ ] Automatic relay discovery and policy-based selection (currently only explicit relay addresses from contacts) 126 + - [ ] Persist connection-quality and address-priority (`last_ok_at`) 127 + 128 + ## 7) Compatibility Notes 129 + Current implementation aligned with `docs/feat/feat_20260206_maep.md` on: 130 + - `peer_id` / `node_id` definitions. 131 + - Key contact-card import validation checks. 132 + - JCS + domain-separator signing. 133 + - JSON profile restrictions (no `null`, no floating-point). 134 + 135 + Current implementation trade-offs: 136 + - Storage lands on files first, with no DB dependency; SQLite remains a future pluggable backend. 137 + - Dedupe uses a rolling window of "7-day TTL + global max 10k records" (processing may happen again after eviction/expiry). 138 + - Protocol constraint: dialogue topics (`share.proactive.v1` / `dm.checkin.v1` / `dm.reply.v1` / `chat.message`) must include `session_id`; `session_id` must be UUIDv7 (plain UUID string, no topic/prefix/suffix). Current behavior validates only format and requiredness, without enforcing session-continuity semantics. 139 + - Storage no longer auto-derives `session_id` from topic, avoiding pseudo-session splits. 140 + - `agent.data.push` is strict envelope-only: `content_type` must start with `application/json`; payload must be envelope JSON (at least `message_id`, `text`, `sent_at`); no transport-layer fallback filling. 141 + - Protocol validation failures are always rejected (`ERR_INVALID_PARAMS`): no automatic plain-text conversion to envelope and no automatic `session_id` generation.
+7
maep/node.go
··· 476 476 req, err := parseRPCRequest(raw) 477 477 if err != nil { 478 478 n.opts.Logger.Warn("invalid rpc request", "peer_id", remotePeerID, "err", err) 479 + if reqID, hasID := extractRPCIDForError(raw); hasID { 480 + symbol := SymbolOf(err) 481 + if strings.TrimSpace(symbol) == "" { 482 + symbol = ErrInvalidParamsSymbol 483 + } 484 + _, _ = n.writeRPCError(stream, reqID, symbol, err.Error()) 485 + } 479 486 return 480 487 } 481 488
+20
maep/rpc.go
··· 213 213 } 214 214 return false 215 215 } 216 + 217 + // Best-effort extraction of request id for parse-error responses. 218 + // This intentionally uses non-strict JSON decode so we can still reply when 219 + // semantic validation fails but a valid id is present. 220 + func extractRPCIDForError(raw []byte) (any, bool) { 221 + if len(bytes.TrimSpace(raw)) == 0 { 222 + return nil, false 223 + } 224 + dec := json.NewDecoder(bytes.NewReader(raw)) 225 + dec.UseNumber() 226 + var obj map[string]any 227 + if err := dec.Decode(&obj); err != nil { 228 + return nil, false 229 + } 230 + idValue, ok := obj["id"] 231 + if !ok || !isValidRPCID(idValue) { 232 + return nil, false 233 + } 234 + return idValue, true 235 + }
+65
maep/rpc_test.go
··· 1 + package maep 2 + 3 + import ( 4 + "encoding/json" 5 + "testing" 6 + ) 7 + 8 + func TestExtractRPCIDForError_StringID(t *testing.T) { 9 + raw := []byte(`{"jsonrpc":"2.0","id":"req-1","method":"agent.ping","params":{}}`) 10 + id, ok := extractRPCIDForError(raw) 11 + if !ok { 12 + t.Fatalf("expected id to be extracted") 13 + } 14 + got, typeOK := id.(string) 15 + if !typeOK { 16 + t.Fatalf("expected string id, got %T", id) 17 + } 18 + if got != "req-1" { 19 + t.Fatalf("id mismatch: got %q want %q", got, "req-1") 20 + } 21 + } 22 + 23 + func TestExtractRPCIDForError_IntegerID(t *testing.T) { 24 + raw := []byte(`{"jsonrpc":"2.0","id":7,"method":"agent.ping","params":{}}`) 25 + id, ok := extractRPCIDForError(raw) 26 + if !ok { 27 + t.Fatalf("expected id to be extracted") 28 + } 29 + got, typeOK := id.(json.Number) 30 + if !typeOK { 31 + t.Fatalf("expected json.Number id, got %T", id) 32 + } 33 + if got.String() != "7" { 34 + t.Fatalf("id mismatch: got %q want %q", got.String(), "7") 35 + } 36 + } 37 + 38 + func TestExtractRPCIDForError_InvalidID(t *testing.T) { 39 + raw := []byte(`{"jsonrpc":"2.0","id":1.5,"method":"agent.ping","params":{}}`) 40 + if _, ok := extractRPCIDForError(raw); ok { 41 + t.Fatalf("expected invalid id to be rejected") 42 + } 43 + } 44 + 45 + func TestExtractRPCIDForError_BestEffortForSemanticFailure(t *testing.T) { 46 + raw := []byte(`{"jsonrpc":"2.0","id":"req-2","params":null}`) 47 + id, ok := extractRPCIDForError(raw) 48 + if !ok { 49 + t.Fatalf("expected id to be extracted") 50 + } 51 + got, typeOK := id.(string) 52 + if !typeOK { 53 + t.Fatalf("expected string id, got %T", id) 54 + } 55 + if got != "req-2" { 56 + t.Fatalf("id mismatch: got %q want %q", got, "req-2") 57 + } 58 + } 59 + 60 + func TestExtractRPCIDForError_InvalidJSON(t *testing.T) { 61 + raw := []byte(`{"jsonrpc":"2.0","id":"req-3",`) 62 + if _, ok := extractRPCIDForError(raw); ok { 63 + t.Fatalf("expected invalid json to return no id") 64 + } 65 + }