openclaw-p2p 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.
- openclaw_p2p-0.1.0/.gitignore +1 -0
- openclaw_p2p-0.1.0/PKG-INFO +333 -0
- openclaw_p2p-0.1.0/README.md +306 -0
- openclaw_p2p-0.1.0/p2p/__init__.py +40 -0
- openclaw_p2p-0.1.0/p2p/cli.py +30 -0
- openclaw_p2p-0.1.0/p2p/config.py +40 -0
- openclaw_p2p-0.1.0/p2p/crypto.py +256 -0
- openclaw_p2p-0.1.0/p2p/errors.py +43 -0
- openclaw_p2p-0.1.0/p2p/handshake.py +326 -0
- openclaw_p2p-0.1.0/p2p/identity.py +145 -0
- openclaw_p2p-0.1.0/p2p/node.py +361 -0
- openclaw_p2p-0.1.0/p2p/peer_store.py +105 -0
- openclaw_p2p-0.1.0/p2p/protocol/__init__.py +5 -0
- openclaw_p2p-0.1.0/p2p/protocol/codec.py +30 -0
- openclaw_p2p-0.1.0/p2p/protocol/envelope.py +27 -0
- openclaw_p2p-0.1.0/p2p/protocol/types.py +151 -0
- openclaw_p2p-0.1.0/p2p/relay_server.py +261 -0
- openclaw_p2p-0.1.0/p2p/transport/__init__.py +3 -0
- openclaw_p2p-0.1.0/p2p/transport/base.py +44 -0
- openclaw_p2p-0.1.0/p2p/transport/relay.py +125 -0
- openclaw_p2p-0.1.0/p2p/transport/websocket.py +276 -0
- openclaw_p2p-0.1.0/pyproject.toml +49 -0
- openclaw_p2p-0.1.0/tests/test_crypto.py +68 -0
- openclaw_p2p-0.1.0/tests/test_handshake.py +186 -0
- openclaw_p2p-0.1.0/tests/test_identity.py +48 -0
- openclaw_p2p-0.1.0/tests/test_peer_store.py +35 -0
- openclaw_p2p-0.1.0/tests/test_protocol.py +38 -0
- openclaw_p2p-0.1.0/tests/test_relay_auth.py +64 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
.env
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: openclaw-p2p
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Agent-to-Agent P2P communication — encrypted, authenticated, relay-capable
|
|
5
|
+
Project-URL: Homepage, https://github.com/openclaw/openclaw-p2p
|
|
6
|
+
Project-URL: Issues, https://github.com/openclaw/openclaw-p2p/issues
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Keywords: agent,ai,ed25519,encryption,noise-protocol,p2p
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Topic :: Communications
|
|
17
|
+
Classifier: Topic :: Security :: Cryptography
|
|
18
|
+
Requires-Python: >=3.11
|
|
19
|
+
Requires-Dist: cryptography>=42.0
|
|
20
|
+
Requires-Dist: msgpack>=1.0
|
|
21
|
+
Requires-Dist: pydantic>=2.0
|
|
22
|
+
Requires-Dist: websockets>=13.0
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
|
|
25
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# openclaw-p2p
|
|
29
|
+
|
|
30
|
+
Encrypted agent-to-agent P2P communication. Let your AI agents talk to each other directly — no platform middleman, no message limits, full E2E encryption.
|
|
31
|
+
|
|
32
|
+
Built for the [OpenClaw](https://github.com/openclaw) agent ecosystem, but works with any Python agent framework.
|
|
33
|
+
|
|
34
|
+
## Install
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install openclaw-p2p
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Quick Start
|
|
41
|
+
|
|
42
|
+
### 1. Generate your agent's identity
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
p2p-keygen
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Output:
|
|
49
|
+
```
|
|
50
|
+
PeerID: p2p:4Tcthedk3w9vSFTmzecPc8vNqqjn
|
|
51
|
+
Keys: ~/.p2p/identity/
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Your PeerID is your agent's public address. Share it with other agent owners to connect.
|
|
55
|
+
|
|
56
|
+
### 2. Run a relay server (or use someone else's)
|
|
57
|
+
|
|
58
|
+
Agents behind NAT need a relay to find each other. The relay is a thin WebSocket proxy — it forwards encrypted frames but **can't read them** (Noise E2E encryption).
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
p2p-relay --port 8765
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
That's it. Run this on any machine with a public IP (a $5/month VPS works). The relay:
|
|
65
|
+
- Authenticates agents via Ed25519 signed registration
|
|
66
|
+
- Pins keys on first use (TOFU) to prevent impersonation
|
|
67
|
+
- Rate limits per peer and per IP
|
|
68
|
+
- Sees only ciphertext — zero access to message content
|
|
69
|
+
|
|
70
|
+
### 3. Connect two agents
|
|
71
|
+
|
|
72
|
+
**Agent A** (your agent):
|
|
73
|
+
```python
|
|
74
|
+
import asyncio
|
|
75
|
+
from p2p import A2ANode, A2AConfig, PeerIdentity, PeerStore, PeerRecord, MessageType
|
|
76
|
+
|
|
77
|
+
async def main():
|
|
78
|
+
identity = PeerIdentity.load("~/.p2p/identity")
|
|
79
|
+
store = PeerStore("~/.p2p/peers.json")
|
|
80
|
+
|
|
81
|
+
# Add agent B as a peer (get their PeerID + relay from their owner)
|
|
82
|
+
store.save(PeerRecord(
|
|
83
|
+
peer_id="p2p:THEIR_PEER_ID_HERE",
|
|
84
|
+
alias="AgentB",
|
|
85
|
+
addresses=["ws://relay.example.com:8765"],
|
|
86
|
+
))
|
|
87
|
+
|
|
88
|
+
node = A2ANode(
|
|
89
|
+
identity=identity,
|
|
90
|
+
peer_store=store,
|
|
91
|
+
config=A2AConfig(relay_addresses=["ws://relay.example.com:8765"]),
|
|
92
|
+
agent_name="AgentA",
|
|
93
|
+
owner_display_name="Alice",
|
|
94
|
+
introduction="Alice's personal assistant",
|
|
95
|
+
capabilities=["calendar", "web_search", "memory"],
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
# Handle incoming messages
|
|
99
|
+
async def on_message(envelope):
|
|
100
|
+
print(f"From {envelope.sender_id}: {envelope.payload}")
|
|
101
|
+
|
|
102
|
+
# Handle new peer approval requests
|
|
103
|
+
async def on_approval(peer_id, hello):
|
|
104
|
+
print(f"New peer wants to connect: {hello['agent_name']} ({hello['owner_display_name']})")
|
|
105
|
+
print(f"PeerID: {peer_id}")
|
|
106
|
+
# In production, ask the owner to approve/reject
|
|
107
|
+
await node.approve_handshake(peer_id, trust_level=2)
|
|
108
|
+
|
|
109
|
+
node.on_message(on_message)
|
|
110
|
+
node.on_approval_needed(on_approval)
|
|
111
|
+
await node.start()
|
|
112
|
+
|
|
113
|
+
# Initiate connection (triggers handshake)
|
|
114
|
+
await node.connect_to_peer("p2p:THEIR_PEER_ID_HERE")
|
|
115
|
+
|
|
116
|
+
# Send a message (after handshake completes)
|
|
117
|
+
await node.send("p2p:THEIR_PEER_ID_HERE", MessageType.TEXT, {"text": "Hello from Agent A!"})
|
|
118
|
+
|
|
119
|
+
await asyncio.Future() # run forever
|
|
120
|
+
|
|
121
|
+
asyncio.run(main())
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
**Agent B** does the same in reverse, with Agent A's PeerID and the same relay address.
|
|
125
|
+
|
|
126
|
+
## What happens when two agents connect
|
|
127
|
+
|
|
128
|
+
```
|
|
129
|
+
1. Both agents connect outbound to the relay (no port forwarding needed)
|
|
130
|
+
2. Noise_XX handshake — mutual authentication, forward-secret session keys
|
|
131
|
+
3. Identity binding proof — Ed25519 signature over handshake hash (proves identity)
|
|
132
|
+
4. HELLO exchange — agent names, capabilities, owner info
|
|
133
|
+
5. Challenge-response — Ed25519 nonce signing (defense-in-depth)
|
|
134
|
+
6. Owner approval — BOTH owners must explicitly approve (mandatory)
|
|
135
|
+
7. ACTIVE — encrypted messaging begins
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Reconnections to known peers skip steps 4-6 (just Noise + binding proof).
|
|
139
|
+
|
|
140
|
+
## Message Types
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
from p2p import MessageType
|
|
144
|
+
|
|
145
|
+
# Basic messaging
|
|
146
|
+
await node.send(peer, MessageType.TEXT, {"text": "Hello!"})
|
|
147
|
+
|
|
148
|
+
# Task delegation
|
|
149
|
+
await node.send(peer, MessageType.TASK_REQUEST, {
|
|
150
|
+
"task": "Review this code",
|
|
151
|
+
"context": "PR #42 in the frontend repo",
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
# Queries
|
|
155
|
+
await node.send(peer, MessageType.QUERY, {
|
|
156
|
+
"text": "What's on my calendar tomorrow?",
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
# File transfer (chunked)
|
|
160
|
+
await node.send(peer, MessageType.FILE_OFFER, {
|
|
161
|
+
"filename": "report.pdf",
|
|
162
|
+
"size_bytes": 1024000,
|
|
163
|
+
"mime_type": "application/pdf",
|
|
164
|
+
"sha256": "abc123...",
|
|
165
|
+
})
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
All messages are signed (Ed25519) and encrypted (AESGCM via Noise session).
|
|
169
|
+
|
|
170
|
+
## Peer Store
|
|
171
|
+
|
|
172
|
+
Known peers are stored in `~/.p2p/peers.json`:
|
|
173
|
+
|
|
174
|
+
```json
|
|
175
|
+
{
|
|
176
|
+
"p2p:7xK9mQ...3bZ": {
|
|
177
|
+
"peer_id": "p2p:7xK9mQ...3bZ",
|
|
178
|
+
"alias": "Nova",
|
|
179
|
+
"trust_level": 2,
|
|
180
|
+
"description": "Alex's research assistant. Connected 2026-03-20 via relay.",
|
|
181
|
+
"capabilities": ["code", "research"],
|
|
182
|
+
"scope_instructions": "Help with research tasks, don't share my calendar",
|
|
183
|
+
"last_seen": 1742515200.0
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
The `description` field is for your agent to write notes about the peer. The `scope_instructions` field controls what your agent should/shouldn't do in conversations with that peer.
|
|
189
|
+
|
|
190
|
+
## Trust Levels
|
|
191
|
+
|
|
192
|
+
| Level | Name | Meaning |
|
|
193
|
+
|-------|------|---------|
|
|
194
|
+
| 0 | Unknown | Never connected, not verified |
|
|
195
|
+
| 1 | Verified | Handshake complete, key exchange done |
|
|
196
|
+
| 2 | Trusted | Owner explicitly upgraded trust |
|
|
197
|
+
| 3 | Allied | Fully trusted (e.g., your own agents on different machines) |
|
|
198
|
+
|
|
199
|
+
Trust upgrades are always manual. Agents start at level 1 after the first handshake.
|
|
200
|
+
|
|
201
|
+
## Self-Hosting a Relay
|
|
202
|
+
|
|
203
|
+
### Quick (for testing)
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
pip install openclaw-p2p
|
|
207
|
+
p2p-relay --port 8765
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### Production (systemd)
|
|
211
|
+
|
|
212
|
+
Create `/etc/systemd/system/p2p-relay.service`:
|
|
213
|
+
|
|
214
|
+
```ini
|
|
215
|
+
[Unit]
|
|
216
|
+
Description=OpenClaw P2P Relay
|
|
217
|
+
After=network.target
|
|
218
|
+
|
|
219
|
+
[Service]
|
|
220
|
+
Type=simple
|
|
221
|
+
User=p2p
|
|
222
|
+
ExecStart=/usr/local/bin/p2p-relay --port 8765
|
|
223
|
+
Restart=always
|
|
224
|
+
RestartSec=5
|
|
225
|
+
|
|
226
|
+
[Install]
|
|
227
|
+
WantedBy=multi-user.target
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
```bash
|
|
231
|
+
sudo systemctl enable --now p2p-relay
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### With TLS (recommended for production)
|
|
235
|
+
|
|
236
|
+
The relay itself runs plain WebSocket. Put it behind a TLS-terminating reverse proxy:
|
|
237
|
+
|
|
238
|
+
**Caddy** (automatic HTTPS):
|
|
239
|
+
```
|
|
240
|
+
relay.yourdomain.com {
|
|
241
|
+
reverse_proxy localhost:8765
|
|
242
|
+
}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
**nginx**:
|
|
246
|
+
```nginx
|
|
247
|
+
server {
|
|
248
|
+
listen 443 ssl;
|
|
249
|
+
server_name relay.yourdomain.com;
|
|
250
|
+
ssl_certificate /path/to/cert.pem;
|
|
251
|
+
ssl_certificate_key /path/to/key.pem;
|
|
252
|
+
|
|
253
|
+
location / {
|
|
254
|
+
proxy_pass http://127.0.0.1:8765;
|
|
255
|
+
proxy_http_version 1.1;
|
|
256
|
+
proxy_set_header Upgrade $http_upgrade;
|
|
257
|
+
proxy_set_header Connection "upgrade";
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
Then agents connect via `wss://relay.yourdomain.com`.
|
|
263
|
+
|
|
264
|
+
> Note: Even without TLS, message content is E2E encrypted (Noise protocol). TLS adds protection for metadata (which PeerIDs are connecting, when, how much traffic).
|
|
265
|
+
|
|
266
|
+
## Security Model
|
|
267
|
+
|
|
268
|
+
| Layer | What it does |
|
|
269
|
+
|-------|-------------|
|
|
270
|
+
| **Noise_XX** | E2E encryption with forward secrecy. Relay sees only ciphertext. |
|
|
271
|
+
| **Identity binding** | Ed25519 signature over Noise handshake hash — proves the PeerID owner is the one in the Noise session |
|
|
272
|
+
| **Challenge-response** | Mutual nonce signing — defense-in-depth on top of Noise |
|
|
273
|
+
| **Message signatures** | Every envelope signed with Ed25519 — verified on receive |
|
|
274
|
+
| **TOFU key pinning** | Relay pins peer_id → public key on first registration |
|
|
275
|
+
| **Timestamped registration** | 60-second freshness window prevents replay of captured registrations |
|
|
276
|
+
| **Owner approval** | Both owners must approve — agents can't autonomously trust each other |
|
|
277
|
+
| **Message dedup** | 10K ring buffer rejects replayed message IDs |
|
|
278
|
+
| **Nonce guard** | Session terminates before AES-GCM nonce can overflow |
|
|
279
|
+
|
|
280
|
+
**What the relay can do**: see which PeerIDs are connected, when, and message sizes (traffic analysis).
|
|
281
|
+
**What the relay can't do**: read messages, forge messages, impersonate agents, or modify traffic without detection.
|
|
282
|
+
|
|
283
|
+
## API Reference
|
|
284
|
+
|
|
285
|
+
### `PeerIdentity`
|
|
286
|
+
|
|
287
|
+
```python
|
|
288
|
+
identity = PeerIdentity.generate() # New random identity
|
|
289
|
+
identity = PeerIdentity.load(path) # Load from ~/.p2p/identity/
|
|
290
|
+
identity.save(path, passphrase="optional") # Save (optionally encrypted)
|
|
291
|
+
identity.peer_id # "p2p:4Tct..."
|
|
292
|
+
identity.sign(data) -> bytes # Ed25519 signature
|
|
293
|
+
PeerIdentity.verify(data, sig, pubkey) -> bool
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
### `A2ANode`
|
|
297
|
+
|
|
298
|
+
```python
|
|
299
|
+
node = A2ANode(identity, peer_store, config,
|
|
300
|
+
agent_name="Name", owner_display_name="Owner",
|
|
301
|
+
capabilities=["list"], introduction="Description")
|
|
302
|
+
|
|
303
|
+
await node.start() # Begin listening + relay registration
|
|
304
|
+
await node.stop() # Graceful shutdown
|
|
305
|
+
await node.send(peer_id, msg_type, payload) # Send message (returns msg ID)
|
|
306
|
+
await node.connect_to_peer(peer_id) # Initiate connection + handshake
|
|
307
|
+
await node.approve_handshake(peer_id, trust_level=1, scope_instructions="")
|
|
308
|
+
await node.reject_handshake(peer_id)
|
|
309
|
+
node.on_message(async_handler) # Register message callback
|
|
310
|
+
node.on_approval_needed(async_handler) # Register approval callback
|
|
311
|
+
node.health() # Returns status dict
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
### `PeerStore`
|
|
315
|
+
|
|
316
|
+
```python
|
|
317
|
+
store = PeerStore("~/.p2p/peers.json")
|
|
318
|
+
store.save(PeerRecord(peer_id="p2p:...", alias="Name", addresses=["ws://..."]))
|
|
319
|
+
store.get(peer_id) -> PeerRecord | None
|
|
320
|
+
store.list_all() -> list[PeerRecord]
|
|
321
|
+
store.remove(peer_id)
|
|
322
|
+
store.update_trust(peer_id, level)
|
|
323
|
+
store.update_description(peer_id, text)
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
## Requirements
|
|
327
|
+
|
|
328
|
+
- Python 3.11+
|
|
329
|
+
- Dependencies: `pydantic`, `cryptography`, `websockets`, `msgpack` (all well-maintained, widely used)
|
|
330
|
+
|
|
331
|
+
## License
|
|
332
|
+
|
|
333
|
+
MIT
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
# openclaw-p2p
|
|
2
|
+
|
|
3
|
+
Encrypted agent-to-agent P2P communication. Let your AI agents talk to each other directly — no platform middleman, no message limits, full E2E encryption.
|
|
4
|
+
|
|
5
|
+
Built for the [OpenClaw](https://github.com/openclaw) agent ecosystem, but works with any Python agent framework.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install openclaw-p2p
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
### 1. Generate your agent's identity
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
p2p-keygen
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Output:
|
|
22
|
+
```
|
|
23
|
+
PeerID: p2p:4Tcthedk3w9vSFTmzecPc8vNqqjn
|
|
24
|
+
Keys: ~/.p2p/identity/
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Your PeerID is your agent's public address. Share it with other agent owners to connect.
|
|
28
|
+
|
|
29
|
+
### 2. Run a relay server (or use someone else's)
|
|
30
|
+
|
|
31
|
+
Agents behind NAT need a relay to find each other. The relay is a thin WebSocket proxy — it forwards encrypted frames but **can't read them** (Noise E2E encryption).
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
p2p-relay --port 8765
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
That's it. Run this on any machine with a public IP (a $5/month VPS works). The relay:
|
|
38
|
+
- Authenticates agents via Ed25519 signed registration
|
|
39
|
+
- Pins keys on first use (TOFU) to prevent impersonation
|
|
40
|
+
- Rate limits per peer and per IP
|
|
41
|
+
- Sees only ciphertext — zero access to message content
|
|
42
|
+
|
|
43
|
+
### 3. Connect two agents
|
|
44
|
+
|
|
45
|
+
**Agent A** (your agent):
|
|
46
|
+
```python
|
|
47
|
+
import asyncio
|
|
48
|
+
from p2p import A2ANode, A2AConfig, PeerIdentity, PeerStore, PeerRecord, MessageType
|
|
49
|
+
|
|
50
|
+
async def main():
|
|
51
|
+
identity = PeerIdentity.load("~/.p2p/identity")
|
|
52
|
+
store = PeerStore("~/.p2p/peers.json")
|
|
53
|
+
|
|
54
|
+
# Add agent B as a peer (get their PeerID + relay from their owner)
|
|
55
|
+
store.save(PeerRecord(
|
|
56
|
+
peer_id="p2p:THEIR_PEER_ID_HERE",
|
|
57
|
+
alias="AgentB",
|
|
58
|
+
addresses=["ws://relay.example.com:8765"],
|
|
59
|
+
))
|
|
60
|
+
|
|
61
|
+
node = A2ANode(
|
|
62
|
+
identity=identity,
|
|
63
|
+
peer_store=store,
|
|
64
|
+
config=A2AConfig(relay_addresses=["ws://relay.example.com:8765"]),
|
|
65
|
+
agent_name="AgentA",
|
|
66
|
+
owner_display_name="Alice",
|
|
67
|
+
introduction="Alice's personal assistant",
|
|
68
|
+
capabilities=["calendar", "web_search", "memory"],
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# Handle incoming messages
|
|
72
|
+
async def on_message(envelope):
|
|
73
|
+
print(f"From {envelope.sender_id}: {envelope.payload}")
|
|
74
|
+
|
|
75
|
+
# Handle new peer approval requests
|
|
76
|
+
async def on_approval(peer_id, hello):
|
|
77
|
+
print(f"New peer wants to connect: {hello['agent_name']} ({hello['owner_display_name']})")
|
|
78
|
+
print(f"PeerID: {peer_id}")
|
|
79
|
+
# In production, ask the owner to approve/reject
|
|
80
|
+
await node.approve_handshake(peer_id, trust_level=2)
|
|
81
|
+
|
|
82
|
+
node.on_message(on_message)
|
|
83
|
+
node.on_approval_needed(on_approval)
|
|
84
|
+
await node.start()
|
|
85
|
+
|
|
86
|
+
# Initiate connection (triggers handshake)
|
|
87
|
+
await node.connect_to_peer("p2p:THEIR_PEER_ID_HERE")
|
|
88
|
+
|
|
89
|
+
# Send a message (after handshake completes)
|
|
90
|
+
await node.send("p2p:THEIR_PEER_ID_HERE", MessageType.TEXT, {"text": "Hello from Agent A!"})
|
|
91
|
+
|
|
92
|
+
await asyncio.Future() # run forever
|
|
93
|
+
|
|
94
|
+
asyncio.run(main())
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
**Agent B** does the same in reverse, with Agent A's PeerID and the same relay address.
|
|
98
|
+
|
|
99
|
+
## What happens when two agents connect
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
1. Both agents connect outbound to the relay (no port forwarding needed)
|
|
103
|
+
2. Noise_XX handshake — mutual authentication, forward-secret session keys
|
|
104
|
+
3. Identity binding proof — Ed25519 signature over handshake hash (proves identity)
|
|
105
|
+
4. HELLO exchange — agent names, capabilities, owner info
|
|
106
|
+
5. Challenge-response — Ed25519 nonce signing (defense-in-depth)
|
|
107
|
+
6. Owner approval — BOTH owners must explicitly approve (mandatory)
|
|
108
|
+
7. ACTIVE — encrypted messaging begins
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Reconnections to known peers skip steps 4-6 (just Noise + binding proof).
|
|
112
|
+
|
|
113
|
+
## Message Types
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
from p2p import MessageType
|
|
117
|
+
|
|
118
|
+
# Basic messaging
|
|
119
|
+
await node.send(peer, MessageType.TEXT, {"text": "Hello!"})
|
|
120
|
+
|
|
121
|
+
# Task delegation
|
|
122
|
+
await node.send(peer, MessageType.TASK_REQUEST, {
|
|
123
|
+
"task": "Review this code",
|
|
124
|
+
"context": "PR #42 in the frontend repo",
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
# Queries
|
|
128
|
+
await node.send(peer, MessageType.QUERY, {
|
|
129
|
+
"text": "What's on my calendar tomorrow?",
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
# File transfer (chunked)
|
|
133
|
+
await node.send(peer, MessageType.FILE_OFFER, {
|
|
134
|
+
"filename": "report.pdf",
|
|
135
|
+
"size_bytes": 1024000,
|
|
136
|
+
"mime_type": "application/pdf",
|
|
137
|
+
"sha256": "abc123...",
|
|
138
|
+
})
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
All messages are signed (Ed25519) and encrypted (AESGCM via Noise session).
|
|
142
|
+
|
|
143
|
+
## Peer Store
|
|
144
|
+
|
|
145
|
+
Known peers are stored in `~/.p2p/peers.json`:
|
|
146
|
+
|
|
147
|
+
```json
|
|
148
|
+
{
|
|
149
|
+
"p2p:7xK9mQ...3bZ": {
|
|
150
|
+
"peer_id": "p2p:7xK9mQ...3bZ",
|
|
151
|
+
"alias": "Nova",
|
|
152
|
+
"trust_level": 2,
|
|
153
|
+
"description": "Alex's research assistant. Connected 2026-03-20 via relay.",
|
|
154
|
+
"capabilities": ["code", "research"],
|
|
155
|
+
"scope_instructions": "Help with research tasks, don't share my calendar",
|
|
156
|
+
"last_seen": 1742515200.0
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
The `description` field is for your agent to write notes about the peer. The `scope_instructions` field controls what your agent should/shouldn't do in conversations with that peer.
|
|
162
|
+
|
|
163
|
+
## Trust Levels
|
|
164
|
+
|
|
165
|
+
| Level | Name | Meaning |
|
|
166
|
+
|-------|------|---------|
|
|
167
|
+
| 0 | Unknown | Never connected, not verified |
|
|
168
|
+
| 1 | Verified | Handshake complete, key exchange done |
|
|
169
|
+
| 2 | Trusted | Owner explicitly upgraded trust |
|
|
170
|
+
| 3 | Allied | Fully trusted (e.g., your own agents on different machines) |
|
|
171
|
+
|
|
172
|
+
Trust upgrades are always manual. Agents start at level 1 after the first handshake.
|
|
173
|
+
|
|
174
|
+
## Self-Hosting a Relay
|
|
175
|
+
|
|
176
|
+
### Quick (for testing)
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
pip install openclaw-p2p
|
|
180
|
+
p2p-relay --port 8765
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### Production (systemd)
|
|
184
|
+
|
|
185
|
+
Create `/etc/systemd/system/p2p-relay.service`:
|
|
186
|
+
|
|
187
|
+
```ini
|
|
188
|
+
[Unit]
|
|
189
|
+
Description=OpenClaw P2P Relay
|
|
190
|
+
After=network.target
|
|
191
|
+
|
|
192
|
+
[Service]
|
|
193
|
+
Type=simple
|
|
194
|
+
User=p2p
|
|
195
|
+
ExecStart=/usr/local/bin/p2p-relay --port 8765
|
|
196
|
+
Restart=always
|
|
197
|
+
RestartSec=5
|
|
198
|
+
|
|
199
|
+
[Install]
|
|
200
|
+
WantedBy=multi-user.target
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
```bash
|
|
204
|
+
sudo systemctl enable --now p2p-relay
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### With TLS (recommended for production)
|
|
208
|
+
|
|
209
|
+
The relay itself runs plain WebSocket. Put it behind a TLS-terminating reverse proxy:
|
|
210
|
+
|
|
211
|
+
**Caddy** (automatic HTTPS):
|
|
212
|
+
```
|
|
213
|
+
relay.yourdomain.com {
|
|
214
|
+
reverse_proxy localhost:8765
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
**nginx**:
|
|
219
|
+
```nginx
|
|
220
|
+
server {
|
|
221
|
+
listen 443 ssl;
|
|
222
|
+
server_name relay.yourdomain.com;
|
|
223
|
+
ssl_certificate /path/to/cert.pem;
|
|
224
|
+
ssl_certificate_key /path/to/key.pem;
|
|
225
|
+
|
|
226
|
+
location / {
|
|
227
|
+
proxy_pass http://127.0.0.1:8765;
|
|
228
|
+
proxy_http_version 1.1;
|
|
229
|
+
proxy_set_header Upgrade $http_upgrade;
|
|
230
|
+
proxy_set_header Connection "upgrade";
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Then agents connect via `wss://relay.yourdomain.com`.
|
|
236
|
+
|
|
237
|
+
> Note: Even without TLS, message content is E2E encrypted (Noise protocol). TLS adds protection for metadata (which PeerIDs are connecting, when, how much traffic).
|
|
238
|
+
|
|
239
|
+
## Security Model
|
|
240
|
+
|
|
241
|
+
| Layer | What it does |
|
|
242
|
+
|-------|-------------|
|
|
243
|
+
| **Noise_XX** | E2E encryption with forward secrecy. Relay sees only ciphertext. |
|
|
244
|
+
| **Identity binding** | Ed25519 signature over Noise handshake hash — proves the PeerID owner is the one in the Noise session |
|
|
245
|
+
| **Challenge-response** | Mutual nonce signing — defense-in-depth on top of Noise |
|
|
246
|
+
| **Message signatures** | Every envelope signed with Ed25519 — verified on receive |
|
|
247
|
+
| **TOFU key pinning** | Relay pins peer_id → public key on first registration |
|
|
248
|
+
| **Timestamped registration** | 60-second freshness window prevents replay of captured registrations |
|
|
249
|
+
| **Owner approval** | Both owners must approve — agents can't autonomously trust each other |
|
|
250
|
+
| **Message dedup** | 10K ring buffer rejects replayed message IDs |
|
|
251
|
+
| **Nonce guard** | Session terminates before AES-GCM nonce can overflow |
|
|
252
|
+
|
|
253
|
+
**What the relay can do**: see which PeerIDs are connected, when, and message sizes (traffic analysis).
|
|
254
|
+
**What the relay can't do**: read messages, forge messages, impersonate agents, or modify traffic without detection.
|
|
255
|
+
|
|
256
|
+
## API Reference
|
|
257
|
+
|
|
258
|
+
### `PeerIdentity`
|
|
259
|
+
|
|
260
|
+
```python
|
|
261
|
+
identity = PeerIdentity.generate() # New random identity
|
|
262
|
+
identity = PeerIdentity.load(path) # Load from ~/.p2p/identity/
|
|
263
|
+
identity.save(path, passphrase="optional") # Save (optionally encrypted)
|
|
264
|
+
identity.peer_id # "p2p:4Tct..."
|
|
265
|
+
identity.sign(data) -> bytes # Ed25519 signature
|
|
266
|
+
PeerIdentity.verify(data, sig, pubkey) -> bool
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
### `A2ANode`
|
|
270
|
+
|
|
271
|
+
```python
|
|
272
|
+
node = A2ANode(identity, peer_store, config,
|
|
273
|
+
agent_name="Name", owner_display_name="Owner",
|
|
274
|
+
capabilities=["list"], introduction="Description")
|
|
275
|
+
|
|
276
|
+
await node.start() # Begin listening + relay registration
|
|
277
|
+
await node.stop() # Graceful shutdown
|
|
278
|
+
await node.send(peer_id, msg_type, payload) # Send message (returns msg ID)
|
|
279
|
+
await node.connect_to_peer(peer_id) # Initiate connection + handshake
|
|
280
|
+
await node.approve_handshake(peer_id, trust_level=1, scope_instructions="")
|
|
281
|
+
await node.reject_handshake(peer_id)
|
|
282
|
+
node.on_message(async_handler) # Register message callback
|
|
283
|
+
node.on_approval_needed(async_handler) # Register approval callback
|
|
284
|
+
node.health() # Returns status dict
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
### `PeerStore`
|
|
288
|
+
|
|
289
|
+
```python
|
|
290
|
+
store = PeerStore("~/.p2p/peers.json")
|
|
291
|
+
store.save(PeerRecord(peer_id="p2p:...", alias="Name", addresses=["ws://..."]))
|
|
292
|
+
store.get(peer_id) -> PeerRecord | None
|
|
293
|
+
store.list_all() -> list[PeerRecord]
|
|
294
|
+
store.remove(peer_id)
|
|
295
|
+
store.update_trust(peer_id, level)
|
|
296
|
+
store.update_description(peer_id, text)
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
## Requirements
|
|
300
|
+
|
|
301
|
+
- Python 3.11+
|
|
302
|
+
- Dependencies: `pydantic`, `cryptography`, `websockets`, `msgpack` (all well-maintained, widely used)
|
|
303
|
+
|
|
304
|
+
## License
|
|
305
|
+
|
|
306
|
+
MIT
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""openclaw-p2p — Agent-to-Agent P2P communication.
|
|
2
|
+
|
|
3
|
+
Encrypted, authenticated, relay-capable communication between AI agents.
|
|
4
|
+
Designed as a pluggable module for OpenClaw and any Python agent framework.
|
|
5
|
+
|
|
6
|
+
Quick start::
|
|
7
|
+
|
|
8
|
+
from p2p import PeerIdentity, A2ANode, PeerStore, A2AConfig
|
|
9
|
+
|
|
10
|
+
identity = PeerIdentity.generate()
|
|
11
|
+
identity.save(Path("~/.p2p/identity"))
|
|
12
|
+
|
|
13
|
+
node = A2ANode(
|
|
14
|
+
identity=identity,
|
|
15
|
+
peer_store=PeerStore("~/.p2p/peers.json"),
|
|
16
|
+
config=A2AConfig(),
|
|
17
|
+
agent_name="MyAgent",
|
|
18
|
+
)
|
|
19
|
+
node.on_message(my_handler)
|
|
20
|
+
await node.start()
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from p2p.config import A2AConfig
|
|
24
|
+
from p2p.identity import PeerIdentity
|
|
25
|
+
from p2p.node import A2ANode
|
|
26
|
+
from p2p.peer_store import PeerRecord, PeerStore
|
|
27
|
+
from p2p.protocol.envelope import A2AEnvelope
|
|
28
|
+
from p2p.protocol.types import MessageType
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
"A2AConfig",
|
|
32
|
+
"A2AEnvelope",
|
|
33
|
+
"A2ANode",
|
|
34
|
+
"MessageType",
|
|
35
|
+
"PeerIdentity",
|
|
36
|
+
"PeerRecord",
|
|
37
|
+
"PeerStore",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
__version__ = "0.1.0"
|