mesh-peer-registry 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Emil Tsoi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,121 @@
1
+ Metadata-Version: 2.4
2
+ Name: mesh-peer-registry
3
+ Version: 0.1.0
4
+ Summary: Shared, language-agnostic peer registry for hermes-mesh and openclaw-mesh with Ed25519 signing.
5
+ Author: Emil Tsoi
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/emiltsoi/mesh-peer-registry
8
+ Project-URL: Repository, https://github.com/emiltsoi/mesh-peer-registry.git
9
+ Project-URL: Issues, https://github.com/emiltsoi/mesh-peer-registry/issues
10
+ Keywords: mesh,peer,registry,ed25519,aiohttp,openclaw,hermes
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Topic :: Communications
19
+ Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
20
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
21
+ Classifier: Typing :: Typed
22
+ Requires-Python: >=3.10
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE
25
+ Requires-Dist: aiohttp>=3.9
26
+ Requires-Dist: cryptography>=41.0
27
+ Provides-Extra: dev
28
+ Requires-Dist: pytest>=8.0; extra == "dev"
29
+ Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
30
+ Dynamic: license-file
31
+
32
+ # mesh-peer-registry
33
+
34
+ A small, shared, language-agnostic peer registry for the mesh network used by [`hermes-mesh`](https://github.com/emiltsoi/hermes-mesh) and [`openclaw-mesh`](https://github.com/emiltsoi/openclaw-mesh).
35
+
36
+ Peers register with an Ed25519 public key and a webhook URL, then discover each other over a simple HTTP API. The server never holds private keys.
37
+
38
+ ## About
39
+
40
+ `mesh-peer-registry` provides a centralized but state-light way for mesh agents to announce themselves to one another:
41
+
42
+ - Peers register an Ed25519 **public key** and a **webhook URL** under a unique name.
43
+ - Registration and deregistration requests are signed by the peer's private key, so the registry can trust the public key it stores.
44
+ - Other peers query the registry to find a recipient's webhook URL and public key.
45
+ - Mesh messages are signed by the sender and verified by the receiver against the sender's public key from the registry.
46
+
47
+ The signing protocol uses compact, deterministic, sorted-key JSON, making it straightforward to implement in other runtimes (e.g. the Node.js implementation in `openclaw-mesh`).
48
+
49
+ ## Features
50
+
51
+ - **HTTP API** for register, list, get, and deregister operations.
52
+ - **Ed25519 signatures** on all mutating requests.
53
+ - **In-memory** store by default, or **file-backed** persistence via `--store`.
54
+ - **CLI** server launcher.
55
+ - **Python client** (`RegistryClient`) with built-in signing.
56
+ - No private keys kept by the registry.
57
+
58
+ ## Quick start
59
+
60
+ Install and run the server:
61
+
62
+ ```bash
63
+ pip install mesh-peer-registry
64
+ mesh-peer-registry --port 8646 --store ./registry.json
65
+ ```
66
+
67
+ The server will listen on `http://127.0.0.1:8646`.
68
+
69
+ ## API
70
+
71
+ - `POST /register` — register or update a peer (signed with the peer's private key).
72
+ - `GET /peers` — list all peers.
73
+ - `GET /peers/{name}` — get one peer.
74
+ - `DELETE /peers/{name}` — deregister a peer (signed with the peer's private key).
75
+
76
+ All registration/deregistration requests must include a valid Ed25519 signature in the `X-Mesh-Signature` header.
77
+
78
+ ## Python client
79
+
80
+ ```python
81
+ from mesh_peer_registry.crypto import generate_keypair
82
+ from mesh_peer_registry.client import RegistryClient
83
+
84
+ private, public = generate_keypair()
85
+ client = RegistryClient("http://127.0.0.1:8646", private, public)
86
+
87
+ client.register(
88
+ "agent0",
89
+ "http://127.0.0.1:8645/mesh/receive",
90
+ role="operator",
91
+ description="Hermes operator node",
92
+ )
93
+
94
+ print(client.list_peers())
95
+ ```
96
+
97
+ ## Verifying a message
98
+
99
+ ```python
100
+ from mesh_peer_registry.crypto import verify_message
101
+ from mesh_peer_registry.client import RegistryClient
102
+
103
+ client = RegistryClient("http://127.0.0.1:8646", "", "")
104
+ peer = client.get_peer("agent0")
105
+
106
+ # body is the raw request body; signature is from the X-Mesh-Signature header.
107
+ ok = verify_message(peer.public_key, body, signature)
108
+ ```
109
+
110
+ ## Development
111
+
112
+ ```bash
113
+ python -m venv .venv
114
+ source .venv/bin/activate
115
+ pip install -e ".[dev]"
116
+ pytest
117
+ ```
118
+
119
+ ## License
120
+
121
+ [MIT](LICENSE)
@@ -0,0 +1,90 @@
1
+ # mesh-peer-registry
2
+
3
+ A small, shared, language-agnostic peer registry for the mesh network used by [`hermes-mesh`](https://github.com/emiltsoi/hermes-mesh) and [`openclaw-mesh`](https://github.com/emiltsoi/openclaw-mesh).
4
+
5
+ Peers register with an Ed25519 public key and a webhook URL, then discover each other over a simple HTTP API. The server never holds private keys.
6
+
7
+ ## About
8
+
9
+ `mesh-peer-registry` provides a centralized but state-light way for mesh agents to announce themselves to one another:
10
+
11
+ - Peers register an Ed25519 **public key** and a **webhook URL** under a unique name.
12
+ - Registration and deregistration requests are signed by the peer's private key, so the registry can trust the public key it stores.
13
+ - Other peers query the registry to find a recipient's webhook URL and public key.
14
+ - Mesh messages are signed by the sender and verified by the receiver against the sender's public key from the registry.
15
+
16
+ The signing protocol uses compact, deterministic, sorted-key JSON, making it straightforward to implement in other runtimes (e.g. the Node.js implementation in `openclaw-mesh`).
17
+
18
+ ## Features
19
+
20
+ - **HTTP API** for register, list, get, and deregister operations.
21
+ - **Ed25519 signatures** on all mutating requests.
22
+ - **In-memory** store by default, or **file-backed** persistence via `--store`.
23
+ - **CLI** server launcher.
24
+ - **Python client** (`RegistryClient`) with built-in signing.
25
+ - No private keys kept by the registry.
26
+
27
+ ## Quick start
28
+
29
+ Install and run the server:
30
+
31
+ ```bash
32
+ pip install mesh-peer-registry
33
+ mesh-peer-registry --port 8646 --store ./registry.json
34
+ ```
35
+
36
+ The server will listen on `http://127.0.0.1:8646`.
37
+
38
+ ## API
39
+
40
+ - `POST /register` — register or update a peer (signed with the peer's private key).
41
+ - `GET /peers` — list all peers.
42
+ - `GET /peers/{name}` — get one peer.
43
+ - `DELETE /peers/{name}` — deregister a peer (signed with the peer's private key).
44
+
45
+ All registration/deregistration requests must include a valid Ed25519 signature in the `X-Mesh-Signature` header.
46
+
47
+ ## Python client
48
+
49
+ ```python
50
+ from mesh_peer_registry.crypto import generate_keypair
51
+ from mesh_peer_registry.client import RegistryClient
52
+
53
+ private, public = generate_keypair()
54
+ client = RegistryClient("http://127.0.0.1:8646", private, public)
55
+
56
+ client.register(
57
+ "agent0",
58
+ "http://127.0.0.1:8645/mesh/receive",
59
+ role="operator",
60
+ description="Hermes operator node",
61
+ )
62
+
63
+ print(client.list_peers())
64
+ ```
65
+
66
+ ## Verifying a message
67
+
68
+ ```python
69
+ from mesh_peer_registry.crypto import verify_message
70
+ from mesh_peer_registry.client import RegistryClient
71
+
72
+ client = RegistryClient("http://127.0.0.1:8646", "", "")
73
+ peer = client.get_peer("agent0")
74
+
75
+ # body is the raw request body; signature is from the X-Mesh-Signature header.
76
+ ok = verify_message(peer.public_key, body, signature)
77
+ ```
78
+
79
+ ## Development
80
+
81
+ ```bash
82
+ python -m venv .venv
83
+ source .venv/bin/activate
84
+ pip install -e ".[dev]"
85
+ pytest
86
+ ```
87
+
88
+ ## License
89
+
90
+ [MIT](LICENSE)
@@ -0,0 +1,26 @@
1
+ """Shared peer registry package for hermes-mesh and openclaw-mesh."""
2
+ from __future__ import annotations
3
+
4
+ from .crypto import (
5
+ canonicalize_payload,
6
+ generate_keypair,
7
+ sign_json,
8
+ sign_message,
9
+ verify_json,
10
+ verify_message,
11
+ )
12
+ from .client import RegistryClient
13
+ from .models import PeerInfo
14
+ from .server import create_app
15
+
16
+ __all__ = [
17
+ "canonicalize_payload",
18
+ "generate_keypair",
19
+ "sign_json",
20
+ "sign_message",
21
+ "verify_json",
22
+ "verify_message",
23
+ "RegistryClient",
24
+ "PeerInfo",
25
+ "create_app",
26
+ ]
@@ -0,0 +1,26 @@
1
+ """CLI entry point for the mesh peer registry server."""
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import os
6
+
7
+ from aiohttp import web
8
+
9
+ from .server import create_app
10
+
11
+ __all__ = ["main"]
12
+
13
+
14
+ def main() -> None:
15
+ parser = argparse.ArgumentParser(description="Run the mesh peer registry")
16
+ parser.add_argument("--host", default="127.0.0.1")
17
+ parser.add_argument("--port", type=int, default=8646)
18
+ parser.add_argument(
19
+ "--store",
20
+ default="~/.mesh/registry.json",
21
+ help="Path to the JSON store file",
22
+ )
23
+ args = parser.parse_args()
24
+
25
+ app = create_app(os.path.expanduser(args.store))
26
+ web.run_app(app, host=args.host, port=args.port)
@@ -0,0 +1,106 @@
1
+ """Synchronous HTTP client for the mesh peer registry."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import urllib.error
6
+ import urllib.request
7
+
8
+ from .crypto import sign_json
9
+ from .models import PeerInfo
10
+
11
+ __all__ = ["RegistryClient"]
12
+
13
+
14
+ class RegistryClient:
15
+ """Synchronous client for a mesh-peer-registry server."""
16
+
17
+ def __init__(
18
+ self,
19
+ registry_url: str,
20
+ private_key_pem: str,
21
+ public_key_pem: str,
22
+ timeout: float = 10.0,
23
+ ) -> None:
24
+ self.registry_url = registry_url.rstrip("/")
25
+ self.private_key_pem = private_key_pem
26
+ self.public_key_pem = public_key_pem
27
+ self.timeout = timeout
28
+
29
+ def _request(
30
+ self,
31
+ method: str,
32
+ path: str,
33
+ payload: dict | None = None,
34
+ signature_payload: dict | None = None,
35
+ ) -> dict:
36
+ url = f"{self.registry_url}{path}"
37
+ headers: dict[str, str] = {}
38
+ data: bytes | None = None
39
+
40
+ if payload is not None:
41
+ data = json.dumps(payload, sort_keys=True).encode("utf-8")
42
+ headers["Content-Type"] = "application/json"
43
+
44
+ if signature_payload is not None:
45
+ sig = sign_json(self.private_key_pem, signature_payload)
46
+ headers["X-Mesh-Signature"] = sig
47
+
48
+ req = urllib.request.Request(
49
+ url, data=data, headers=headers, method=method
50
+ )
51
+ try:
52
+ with urllib.request.urlopen(req, timeout=self.timeout) as resp:
53
+ return json.loads(resp.read().decode("utf-8"))
54
+ except urllib.error.HTTPError as exc:
55
+ body = exc.read().decode("utf-8")
56
+ try:
57
+ detail = json.loads(body)
58
+ except json.JSONDecodeError:
59
+ detail = body
60
+ raise RegistryClientError(
61
+ status=exc.code, message=detail
62
+ ) from exc
63
+
64
+ def register(
65
+ self,
66
+ name: str,
67
+ url: str,
68
+ role: str = "agent",
69
+ description: str = "",
70
+ ) -> dict:
71
+ payload = {
72
+ "name": name,
73
+ "url": url,
74
+ "public_key": self.public_key_pem,
75
+ "role": role,
76
+ "description": description,
77
+ }
78
+ return self._request("POST", "/register", payload, payload)
79
+
80
+ def list_peers(self) -> list[PeerInfo]:
81
+ data = self._request("GET", "/peers")
82
+ return [PeerInfo(**p) for p in data.get("peers", [])]
83
+
84
+ def get_peer(self, name: str) -> PeerInfo | None:
85
+ try:
86
+ data = self._request("GET", f"/peers/{name}")
87
+ return PeerInfo(**data)
88
+ except RegistryClientError as exc:
89
+ if exc.status == 404:
90
+ return None
91
+ raise
92
+
93
+ def deregister(self, name: str) -> dict:
94
+ sig_payload = {"name": name, "action": "deregister"}
95
+ return self._request(
96
+ "DELETE", f"/peers/{name}", signature_payload=sig_payload
97
+ )
98
+
99
+
100
+ class RegistryClientError(Exception):
101
+ """Registry client error with HTTP status and server message."""
102
+
103
+ def __init__(self, status: int, message: object):
104
+ self.status = status
105
+ self.message = message
106
+ super().__init__(f"Registry HTTP {status}: {message}")
@@ -0,0 +1,87 @@
1
+ """Ed25519 signature primitives for the mesh peer registry."""
2
+ from __future__ import annotations
3
+
4
+ import base64
5
+ import json
6
+ from typing import Union
7
+
8
+ from cryptography.exceptions import InvalidSignature
9
+ from cryptography.hazmat.primitives import serialization
10
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import (
11
+ Ed25519PrivateKey,
12
+ Ed25519PublicKey,
13
+ )
14
+
15
+ __all__ = [
16
+ "generate_keypair",
17
+ "sign_message",
18
+ "verify_message",
19
+ "canonicalize_payload",
20
+ "sign_json",
21
+ "verify_json",
22
+ ]
23
+
24
+
25
+ def generate_keypair() -> tuple[str, str]:
26
+ """Generate a new Ed25519 keypair and return (private_key_pem, public_key_pem)."""
27
+ private_key = Ed25519PrivateKey.generate()
28
+ public_key = private_key.public_key()
29
+ private_pem = private_key.private_bytes(
30
+ encoding=serialization.Encoding.PEM,
31
+ format=serialization.PrivateFormat.PKCS8,
32
+ encryption_algorithm=serialization.NoEncryption(),
33
+ ).decode("utf-8")
34
+ public_pem = public_key.public_bytes(
35
+ encoding=serialization.Encoding.PEM,
36
+ format=serialization.PublicFormat.SubjectPublicKeyInfo,
37
+ ).decode("utf-8")
38
+ return private_pem, public_pem
39
+
40
+
41
+ def _load_private_key(private_key_pem: str) -> Ed25519PrivateKey:
42
+ return serialization.load_pem_private_key(
43
+ private_key_pem.encode("utf-8"), password=None
44
+ )
45
+
46
+
47
+ def _load_public_key(public_key_pem: str) -> Ed25519PublicKey:
48
+ return serialization.load_pem_public_key(public_key_pem.encode("utf-8"))
49
+
50
+
51
+ def sign_message(private_key_pem: str, message: Union[str, bytes]) -> str:
52
+ """Sign a message and return a base64-encoded signature."""
53
+ if isinstance(message, str):
54
+ message = message.encode("utf-8")
55
+ private_key = _load_private_key(private_key_pem)
56
+ signature = private_key.sign(message)
57
+ return base64.b64encode(signature).decode("utf-8")
58
+
59
+
60
+ def verify_message(
61
+ public_key_pem: str, message: Union[str, bytes], signature_b64: str
62
+ ) -> bool:
63
+ """Verify a base64-encoded signature against a message and public key."""
64
+ if isinstance(message, str):
65
+ message = message.encode("utf-8")
66
+ public_key = _load_public_key(public_key_pem)
67
+ signature = base64.b64decode(signature_b64.encode("utf-8"))
68
+ try:
69
+ public_key.verify(signature, message)
70
+ return True
71
+ except InvalidSignature:
72
+ return False
73
+
74
+
75
+ def canonicalize_payload(payload: dict) -> bytes:
76
+ """Return a deterministic JSON encoding of a payload for signing."""
77
+ return json.dumps(payload, sort_keys=True, separators=(",", ":")).encode("utf-8")
78
+
79
+
80
+ def sign_json(private_key_pem: str, payload: dict) -> str:
81
+ """Sign a JSON-serializable payload and return a base64-encoded signature."""
82
+ return sign_message(private_key_pem, canonicalize_payload(payload))
83
+
84
+
85
+ def verify_json(public_key_pem: str, payload: dict, signature_b64: str) -> bool:
86
+ """Verify a signature over a JSON-serializable payload."""
87
+ return verify_message(public_key_pem, canonicalize_payload(payload), signature_b64)
@@ -0,0 +1,15 @@
1
+ """Data models for the mesh peer registry."""
2
+ from __future__ import annotations
3
+
4
+ from dataclasses import dataclass
5
+
6
+
7
+ @dataclass
8
+ class PeerInfo:
9
+ """Public peer record held by the registry."""
10
+
11
+ name: str
12
+ url: str
13
+ public_key: str
14
+ role: str = "agent"
15
+ description: str = ""
@@ -0,0 +1,97 @@
1
+ """HTTP registry server for mesh peers."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import os
6
+ from pathlib import Path
7
+
8
+ from aiohttp import web
9
+
10
+ from .crypto import verify_json
11
+ from .models import PeerInfo
12
+ from .store import FileStore
13
+
14
+ __all__ = ["create_app"]
15
+
16
+ AppKeyStore = web.AppKey("store", FileStore)
17
+
18
+ REGISTRY_FIELDS = {"name", "url", "public_key", "role", "description"}
19
+ REQUIRED_FIELDS = {"name", "url", "public_key"}
20
+
21
+
22
+ async def _json_body(request: web.Request) -> dict:
23
+ try:
24
+ return await request.json()
25
+ except json.JSONDecodeError:
26
+ raise web.HTTPBadRequest(text='{"error": "invalid json"}')
27
+
28
+
29
+ async def register(request: web.Request) -> web.Response:
30
+ body = await _json_body(request)
31
+ missing = REQUIRED_FIELDS - set(body)
32
+ if missing:
33
+ return web.json_response(
34
+ {"error": f"missing fields: {sorted(missing)}"}, status=400
35
+ )
36
+
37
+ sig = request.headers.get("X-Mesh-Signature", "")
38
+ if not sig:
39
+ return web.json_response(
40
+ {"error": "missing X-Mesh-Signature header"}, status=400
41
+ )
42
+
43
+ payload = {k: body[k] for k in REGISTRY_FIELDS if k in body}
44
+ public_key = body["public_key"]
45
+ if not verify_json(public_key, payload, sig):
46
+ return web.json_response({"error": "invalid signature"}, status=401)
47
+
48
+ store: FileStore = request.app[AppKeyStore]
49
+ store.put(PeerInfo(**payload))
50
+ return web.json_response({"ok": True, "peer": payload})
51
+
52
+
53
+ async def list_peers(request: web.Request) -> web.Response:
54
+ store: FileStore = request.app[AppKeyStore]
55
+ peers = store.list()
56
+ return web.json_response({"peers": [p.__dict__ for p in peers]})
57
+
58
+
59
+ async def get_peer(request: web.Request) -> web.Response:
60
+ name = request.match_info["name"]
61
+ store: FileStore = request.app[AppKeyStore]
62
+ peer = store.get(name)
63
+ if not peer:
64
+ return web.json_response({"error": "not found"}, status=404)
65
+ return web.json_response(peer.__dict__)
66
+
67
+
68
+ async def delete_peer(request: web.Request) -> web.Response:
69
+ name = request.match_info["name"]
70
+ sig = request.headers.get("X-Mesh-Signature", "")
71
+ if not sig:
72
+ return web.json_response(
73
+ {"error": "missing X-Mesh-Signature header"}, status=400
74
+ )
75
+
76
+ store: FileStore = request.app[AppKeyStore]
77
+ peer = store.get(name)
78
+ if not peer:
79
+ return web.json_response({"error": "not found"}, status=404)
80
+
81
+ sig_payload = {"name": name, "action": "deregister"}
82
+ if not verify_json(peer.public_key, sig_payload, sig):
83
+ return web.json_response({"error": "invalid signature"}, status=401)
84
+
85
+ store.delete(name)
86
+ return web.json_response({"ok": True})
87
+
88
+
89
+ def create_app(store_path: str | None = None) -> web.Application:
90
+ store_path = store_path or os.path.expanduser("~/.mesh/registry.json")
91
+ app = web.Application()
92
+ app[AppKeyStore] = FileStore(Path(store_path))
93
+ app.router.add_post("/register", register)
94
+ app.router.add_get("/peers", list_peers)
95
+ app.router.add_get("/peers/{name}", get_peer)
96
+ app.router.add_delete("/peers/{name}", delete_peer)
97
+ return app
@@ -0,0 +1,56 @@
1
+ """Persistence layer for the mesh peer registry."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import os
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from .models import PeerInfo
10
+
11
+
12
+ class FileStore:
13
+ """Atomic JSON file storage for peer records."""
14
+
15
+ def __init__(self, path: str | Path) -> None:
16
+ self.path = Path(path).expanduser()
17
+ self.path.parent.mkdir(parents=True, exist_ok=True)
18
+
19
+ def _load(self) -> dict[str, Any]:
20
+ if not self.path.exists():
21
+ return {}
22
+ try:
23
+ return json.loads(self.path.read_text(encoding="utf-8"))
24
+ except (json.JSONDecodeError, OSError):
25
+ return {}
26
+
27
+ def _save(self, data: dict[str, Any]) -> None:
28
+ tmp = self.path.with_suffix(".tmp")
29
+ tmp.write_text(
30
+ json.dumps(data, sort_keys=True, indent=2),
31
+ encoding="utf-8",
32
+ )
33
+ os.replace(tmp, self.path)
34
+
35
+ def get(self, name: str) -> PeerInfo | None:
36
+ data = self._load()
37
+ if name not in data:
38
+ return None
39
+ return PeerInfo(**data[name])
40
+
41
+ def list(self) -> list[PeerInfo]:
42
+ data = self._load()
43
+ return [PeerInfo(**v) for _, v in sorted(data.items())]
44
+
45
+ def put(self, peer: PeerInfo) -> None:
46
+ data = self._load()
47
+ data[peer.name] = peer.__dict__
48
+ self._save(data)
49
+
50
+ def delete(self, name: str) -> bool:
51
+ data = self._load()
52
+ if name not in data:
53
+ return False
54
+ del data[name]
55
+ self._save(data)
56
+ return True
@@ -0,0 +1,121 @@
1
+ Metadata-Version: 2.4
2
+ Name: mesh-peer-registry
3
+ Version: 0.1.0
4
+ Summary: Shared, language-agnostic peer registry for hermes-mesh and openclaw-mesh with Ed25519 signing.
5
+ Author: Emil Tsoi
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/emiltsoi/mesh-peer-registry
8
+ Project-URL: Repository, https://github.com/emiltsoi/mesh-peer-registry.git
9
+ Project-URL: Issues, https://github.com/emiltsoi/mesh-peer-registry/issues
10
+ Keywords: mesh,peer,registry,ed25519,aiohttp,openclaw,hermes
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Topic :: Communications
19
+ Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
20
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
21
+ Classifier: Typing :: Typed
22
+ Requires-Python: >=3.10
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE
25
+ Requires-Dist: aiohttp>=3.9
26
+ Requires-Dist: cryptography>=41.0
27
+ Provides-Extra: dev
28
+ Requires-Dist: pytest>=8.0; extra == "dev"
29
+ Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
30
+ Dynamic: license-file
31
+
32
+ # mesh-peer-registry
33
+
34
+ A small, shared, language-agnostic peer registry for the mesh network used by [`hermes-mesh`](https://github.com/emiltsoi/hermes-mesh) and [`openclaw-mesh`](https://github.com/emiltsoi/openclaw-mesh).
35
+
36
+ Peers register with an Ed25519 public key and a webhook URL, then discover each other over a simple HTTP API. The server never holds private keys.
37
+
38
+ ## About
39
+
40
+ `mesh-peer-registry` provides a centralized but state-light way for mesh agents to announce themselves to one another:
41
+
42
+ - Peers register an Ed25519 **public key** and a **webhook URL** under a unique name.
43
+ - Registration and deregistration requests are signed by the peer's private key, so the registry can trust the public key it stores.
44
+ - Other peers query the registry to find a recipient's webhook URL and public key.
45
+ - Mesh messages are signed by the sender and verified by the receiver against the sender's public key from the registry.
46
+
47
+ The signing protocol uses compact, deterministic, sorted-key JSON, making it straightforward to implement in other runtimes (e.g. the Node.js implementation in `openclaw-mesh`).
48
+
49
+ ## Features
50
+
51
+ - **HTTP API** for register, list, get, and deregister operations.
52
+ - **Ed25519 signatures** on all mutating requests.
53
+ - **In-memory** store by default, or **file-backed** persistence via `--store`.
54
+ - **CLI** server launcher.
55
+ - **Python client** (`RegistryClient`) with built-in signing.
56
+ - No private keys kept by the registry.
57
+
58
+ ## Quick start
59
+
60
+ Install and run the server:
61
+
62
+ ```bash
63
+ pip install mesh-peer-registry
64
+ mesh-peer-registry --port 8646 --store ./registry.json
65
+ ```
66
+
67
+ The server will listen on `http://127.0.0.1:8646`.
68
+
69
+ ## API
70
+
71
+ - `POST /register` — register or update a peer (signed with the peer's private key).
72
+ - `GET /peers` — list all peers.
73
+ - `GET /peers/{name}` — get one peer.
74
+ - `DELETE /peers/{name}` — deregister a peer (signed with the peer's private key).
75
+
76
+ All registration/deregistration requests must include a valid Ed25519 signature in the `X-Mesh-Signature` header.
77
+
78
+ ## Python client
79
+
80
+ ```python
81
+ from mesh_peer_registry.crypto import generate_keypair
82
+ from mesh_peer_registry.client import RegistryClient
83
+
84
+ private, public = generate_keypair()
85
+ client = RegistryClient("http://127.0.0.1:8646", private, public)
86
+
87
+ client.register(
88
+ "agent0",
89
+ "http://127.0.0.1:8645/mesh/receive",
90
+ role="operator",
91
+ description="Hermes operator node",
92
+ )
93
+
94
+ print(client.list_peers())
95
+ ```
96
+
97
+ ## Verifying a message
98
+
99
+ ```python
100
+ from mesh_peer_registry.crypto import verify_message
101
+ from mesh_peer_registry.client import RegistryClient
102
+
103
+ client = RegistryClient("http://127.0.0.1:8646", "", "")
104
+ peer = client.get_peer("agent0")
105
+
106
+ # body is the raw request body; signature is from the X-Mesh-Signature header.
107
+ ok = verify_message(peer.public_key, body, signature)
108
+ ```
109
+
110
+ ## Development
111
+
112
+ ```bash
113
+ python -m venv .venv
114
+ source .venv/bin/activate
115
+ pip install -e ".[dev]"
116
+ pytest
117
+ ```
118
+
119
+ ## License
120
+
121
+ [MIT](LICENSE)
@@ -0,0 +1,17 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ mesh_peer_registry/__init__.py
5
+ mesh_peer_registry/cli.py
6
+ mesh_peer_registry/client.py
7
+ mesh_peer_registry/crypto.py
8
+ mesh_peer_registry/models.py
9
+ mesh_peer_registry/server.py
10
+ mesh_peer_registry/store.py
11
+ mesh_peer_registry.egg-info/PKG-INFO
12
+ mesh_peer_registry.egg-info/SOURCES.txt
13
+ mesh_peer_registry.egg-info/dependency_links.txt
14
+ mesh_peer_registry.egg-info/entry_points.txt
15
+ mesh_peer_registry.egg-info/requires.txt
16
+ mesh_peer_registry.egg-info/top_level.txt
17
+ tests/test_registry.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ mesh-peer-registry = mesh_peer_registry.cli:main
@@ -0,0 +1,6 @@
1
+ aiohttp>=3.9
2
+ cryptography>=41.0
3
+
4
+ [dev]
5
+ pytest>=8.0
6
+ pytest-asyncio>=0.23
@@ -0,0 +1 @@
1
+ mesh_peer_registry
@@ -0,0 +1,48 @@
1
+ [project]
2
+ name = "mesh-peer-registry"
3
+ version = "0.1.0"
4
+ description = "Shared, language-agnostic peer registry for hermes-mesh and openclaw-mesh with Ed25519 signing."
5
+ readme = "README.md"
6
+ license = {text = "MIT"}
7
+ requires-python = ">=3.10"
8
+ authors = [{name = "Emil Tsoi"}]
9
+ keywords = ["mesh", "peer", "registry", "ed25519", "aiohttp", "openclaw", "hermes"]
10
+ classifiers = [
11
+ "Development Status :: 4 - Beta",
12
+ "Intended Audience :: Developers",
13
+ "License :: OSI Approved :: MIT License",
14
+ "Programming Language :: Python :: 3",
15
+ "Programming Language :: Python :: 3.10",
16
+ "Programming Language :: Python :: 3.11",
17
+ "Programming Language :: Python :: 3.12",
18
+ "Topic :: Communications",
19
+ "Topic :: Internet :: WWW/HTTP :: HTTP Servers",
20
+ "Topic :: Software Development :: Libraries :: Python Modules",
21
+ "Typing :: Typed",
22
+ ]
23
+ dependencies = [
24
+ "aiohttp>=3.9",
25
+ "cryptography>=41.0",
26
+ ]
27
+
28
+ [project.optional-dependencies]
29
+ dev = [
30
+ "pytest>=8.0",
31
+ "pytest-asyncio>=0.23",
32
+ ]
33
+
34
+ [project.urls]
35
+ Homepage = "https://github.com/emiltsoi/mesh-peer-registry"
36
+ Repository = "https://github.com/emiltsoi/mesh-peer-registry.git"
37
+ Issues = "https://github.com/emiltsoi/mesh-peer-registry/issues"
38
+
39
+ [project.scripts]
40
+ mesh-peer-registry = "mesh_peer_registry.cli:main"
41
+
42
+ [build-system]
43
+ requires = ["setuptools>=68"]
44
+ build-backend = "setuptools.build_meta"
45
+
46
+ [tool.setuptools]
47
+ packages = ["mesh_peer_registry"]
48
+ license-files = ["LICENSE"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,159 @@
1
+ """Tests for mesh-peer-registry crypto, store, and server."""
2
+ from __future__ import annotations
3
+
4
+ import asyncio
5
+ import json
6
+ import tempfile
7
+ import unittest
8
+ from pathlib import Path
9
+
10
+ from aiohttp.test_utils import AioHTTPTestCase
11
+
12
+ from mesh_peer_registry.client import RegistryClient, RegistryClientError
13
+ from mesh_peer_registry.crypto import (
14
+ canonicalize_payload,
15
+ generate_keypair,
16
+ sign_json,
17
+ sign_message,
18
+ verify_json,
19
+ verify_message,
20
+ )
21
+ from mesh_peer_registry.models import PeerInfo
22
+ from mesh_peer_registry.server import create_app
23
+ from mesh_peer_registry.store import FileStore
24
+
25
+
26
+ class TestCrypto(unittest.TestCase):
27
+ def test_generate_keypair(self):
28
+ private, public = generate_keypair()
29
+ assert "BEGIN PRIVATE KEY" in private
30
+ assert "BEGIN PUBLIC KEY" in public
31
+
32
+ def test_sign_and_verify(self):
33
+ private, public = generate_keypair()
34
+ sig = sign_message(private, "hello mesh")
35
+ assert verify_message(public, "hello mesh", sig)
36
+
37
+ def test_verify_fails_with_wrong_key(self):
38
+ private, public = generate_keypair()
39
+ _, wrong_public = generate_keypair()
40
+ sig = sign_message(private, "hello")
41
+ assert not verify_message(wrong_public, "hello", sig)
42
+
43
+ def test_verify_fails_with_tampered_message(self):
44
+ private, public = generate_keypair()
45
+ sig = sign_message(private, "hello")
46
+ assert not verify_message(public, "tampered", sig)
47
+
48
+ def test_sign_and_verify_json(self):
49
+ private, public = generate_keypair()
50
+ payload = {"from": "linda", "to": "britney", "id": "msg-123"}
51
+ sig = sign_json(private, payload)
52
+ assert verify_json(public, payload, sig)
53
+ assert not verify_json(public, {"from": "linda"}, sig)
54
+
55
+ def test_canonicalize_is_deterministic(self):
56
+ a = canonicalize_payload({"b": 2, "a": 1})
57
+ b = canonicalize_payload({"a": 1, "b": 2})
58
+ assert a == b
59
+
60
+
61
+ class TestStore(unittest.TestCase):
62
+ def test_put_get_list_delete(self):
63
+ with tempfile.TemporaryDirectory() as tmpdir:
64
+ store = FileStore(Path(tmpdir) / "registry.json")
65
+ peer = PeerInfo(
66
+ name="britney",
67
+ url="http://127.0.0.1:8645",
68
+ public_key="pk",
69
+ role="swe",
70
+ )
71
+ store.put(peer)
72
+ fetched = store.get("britney")
73
+ assert fetched is not None
74
+ assert fetched.name == "britney"
75
+ assert store.list()[0].name == "britney"
76
+ assert store.delete("britney")
77
+ assert store.get("britney") is None
78
+
79
+ def test_delete_missing_returns_false(self):
80
+ with tempfile.TemporaryDirectory() as tmpdir:
81
+ store = FileStore(Path(tmpdir) / "registry.json")
82
+ assert not store.delete("nobody")
83
+
84
+
85
+ class TestRegistryServer(AioHTTPTestCase):
86
+ async def get_application(self):
87
+ self.tmpdir = tempfile.TemporaryDirectory()
88
+ return create_app(str(Path(self.tmpdir.name) / "registry.json"))
89
+
90
+ def tearDown(self):
91
+ super().tearDown()
92
+ if hasattr(self, "tmpdir"):
93
+ self.tmpdir.cleanup()
94
+
95
+ async def test_list_empty(self):
96
+ resp = await self.client.request("GET", "/peers")
97
+ assert resp.status == 200
98
+ body = await resp.json()
99
+ assert body["peers"] == []
100
+
101
+ async def test_register_and_get(self):
102
+ private, public = generate_keypair()
103
+ client = RegistryClient(
104
+ f"http://127.0.0.1:{self.server.port}",
105
+ private,
106
+ public,
107
+ )
108
+
109
+ result = await asyncio.to_thread(
110
+ client.register,
111
+ name="britney",
112
+ url="http://127.0.0.1:8645",
113
+ role="swe",
114
+ description="Principal",
115
+ )
116
+ assert result["ok"]
117
+
118
+ peer = await asyncio.to_thread(client.get_peer, "britney")
119
+ assert peer is not None
120
+ assert peer.name == "britney"
121
+ assert peer.role == "swe"
122
+
123
+ async def test_register_without_signature_rejected(self):
124
+ resp = await self.client.request(
125
+ "POST",
126
+ "/register",
127
+ data=json.dumps(
128
+ {
129
+ "name": "britney",
130
+ "url": "http://127.0.0.1:8645",
131
+ "public_key": "pk",
132
+ }
133
+ ),
134
+ headers={"Content-Type": "application/json"},
135
+ )
136
+ assert resp.status == 400
137
+
138
+ async def test_deregister(self):
139
+ private, public = generate_keypair()
140
+ client = RegistryClient(
141
+ f"http://127.0.0.1:{self.server.port}",
142
+ private,
143
+ public,
144
+ )
145
+
146
+ await asyncio.to_thread(
147
+ client.register,
148
+ name="daji",
149
+ url="http://127.0.0.1:8646",
150
+ role="agent",
151
+ )
152
+ assert (await asyncio.to_thread(client.get_peer, "daji")) is not None
153
+
154
+ await asyncio.to_thread(client.deregister, "daji")
155
+ assert (await asyncio.to_thread(client.get_peer, "daji")) is None
156
+
157
+ async def test_get_missing_returns_404(self):
158
+ resp = await self.client.request("GET", "/peers/nobody")
159
+ assert resp.status == 404