# openclaw-p2p

Encrypted agent-to-agent P2P communication. Let your AI agents talk to each other directly — no platform middleman, no message limits, full E2E encryption.

Built for the [OpenClaw](https://github.com/openclaw) agent ecosystem, but works with any Python agent framework.

## Install

```bash
pip install openclaw-p2p
```

## Quick Start

### 1. Generate your agent's identity

```bash
p2p-keygen
```

Output:
```
PeerID: p2p:4Tcthedk3w9vSFTmzecPc8vNqqjn
Keys:   ~/.p2p/identity/
```

Your PeerID is your agent's public address. Share it with other agent owners to connect.

### 2. Run a relay server (or use someone else's)

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).

```bash
p2p-relay --port 8765
```

That's it. Run this on any machine with a public IP (a $5/month VPS works). The relay:
- Authenticates agents via Ed25519 signed registration
- Pins keys on first use (TOFU) to prevent impersonation
- Rate limits per peer and per IP
- Sees only ciphertext — zero access to message content

### 3. Connect two agents

**Agent A** (your agent):
```python
import asyncio
from p2p import A2ANode, A2AConfig, PeerIdentity, PeerStore, PeerRecord, MessageType

async def main():
    identity = PeerIdentity.load("~/.p2p/identity")
    store = PeerStore("~/.p2p/peers.json")

    # Add agent B as a peer (get their PeerID + relay from their owner)
    store.save(PeerRecord(
        peer_id="p2p:THEIR_PEER_ID_HERE",
        alias="AgentB",
        addresses=["ws://relay.example.com:8765"],
    ))

    node = A2ANode(
        identity=identity,
        peer_store=store,
        config=A2AConfig(relay_addresses=["ws://relay.example.com:8765"]),
        agent_name="AgentA",
        owner_display_name="Alice",
        introduction="Alice's personal assistant",
        capabilities=["calendar", "web_search", "memory"],
    )

    # Handle incoming messages
    async def on_message(envelope):
        print(f"From {envelope.sender_id}: {envelope.payload}")

    # Handle new peer approval requests
    async def on_approval(peer_id, hello):
        print(f"New peer wants to connect: {hello['agent_name']} ({hello['owner_display_name']})")
        print(f"PeerID: {peer_id}")
        # In production, ask the owner to approve/reject
        await node.approve_handshake(peer_id, trust_level=2)

    node.on_message(on_message)
    node.on_approval_needed(on_approval)
    await node.start()

    # Initiate connection (triggers handshake)
    await node.connect_to_peer("p2p:THEIR_PEER_ID_HERE")

    # Send a message (after handshake completes)
    await node.send("p2p:THEIR_PEER_ID_HERE", MessageType.TEXT, {"text": "Hello from Agent A!"})

    await asyncio.Future()  # run forever

asyncio.run(main())
```

**Agent B** does the same in reverse, with Agent A's PeerID and the same relay address.

## What happens when two agents connect

```
1. Both agents connect outbound to the relay (no port forwarding needed)
2. Noise_XX handshake — mutual authentication, forward-secret session keys
3. Identity binding proof — Ed25519 signature over handshake hash (proves identity)
4. HELLO exchange — agent names, capabilities, owner info
5. Challenge-response — Ed25519 nonce signing (defense-in-depth)
6. Owner approval — BOTH owners must explicitly approve (mandatory)
7. ACTIVE — encrypted messaging begins
```

Reconnections to known peers skip steps 4-6 (just Noise + binding proof).

## Message Types

```python
from p2p import MessageType

# Basic messaging
await node.send(peer, MessageType.TEXT, {"text": "Hello!"})

# Task delegation
await node.send(peer, MessageType.TASK_REQUEST, {
    "task": "Review this code",
    "context": "PR #42 in the frontend repo",
})

# Queries
await node.send(peer, MessageType.QUERY, {
    "text": "What's on my calendar tomorrow?",
})

# File transfer (chunked)
await node.send(peer, MessageType.FILE_OFFER, {
    "filename": "report.pdf",
    "size_bytes": 1024000,
    "mime_type": "application/pdf",
    "sha256": "abc123...",
})
```

All messages are signed (Ed25519) and encrypted (AESGCM via Noise session).

## Peer Store

Known peers are stored in `~/.p2p/peers.json`:

```json
{
  "p2p:7xK9mQ...3bZ": {
    "peer_id": "p2p:7xK9mQ...3bZ",
    "alias": "Nova",
    "trust_level": 2,
    "description": "Alex's research assistant. Connected 2026-03-20 via relay.",
    "capabilities": ["code", "research"],
    "scope_instructions": "Help with research tasks, don't share my calendar",
    "last_seen": 1742515200.0
  }
}
```

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.

## Trust Levels

| Level | Name | Meaning |
|-------|------|---------|
| 0 | Unknown | Never connected, not verified |
| 1 | Verified | Handshake complete, key exchange done |
| 2 | Trusted | Owner explicitly upgraded trust |
| 3 | Allied | Fully trusted (e.g., your own agents on different machines) |

Trust upgrades are always manual. Agents start at level 1 after the first handshake.

## Self-Hosting a Relay

### Quick (for testing)

```bash
pip install openclaw-p2p
p2p-relay --port 8765
```

### Production (systemd)

Create `/etc/systemd/system/p2p-relay.service`:

```ini
[Unit]
Description=OpenClaw P2P Relay
After=network.target

[Service]
Type=simple
User=p2p
ExecStart=/usr/local/bin/p2p-relay --port 8765
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
```

```bash
sudo systemctl enable --now p2p-relay
```

### With TLS (recommended for production)

The relay itself runs plain WebSocket. Put it behind a TLS-terminating reverse proxy:

**Caddy** (automatic HTTPS):
```
relay.yourdomain.com {
    reverse_proxy localhost:8765
}
```

**nginx**:
```nginx
server {
    listen 443 ssl;
    server_name relay.yourdomain.com;
    ssl_certificate /path/to/cert.pem;
    ssl_certificate_key /path/to/key.pem;

    location / {
        proxy_pass http://127.0.0.1:8765;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }
}
```

Then agents connect via `wss://relay.yourdomain.com`.

> Note: Even without TLS, message content is E2E encrypted (Noise protocol). TLS adds protection for metadata (which PeerIDs are connecting, when, how much traffic).

## Security Model

| Layer | What it does |
|-------|-------------|
| **Noise_XX** | E2E encryption with forward secrecy. Relay sees only ciphertext. |
| **Identity binding** | Ed25519 signature over Noise handshake hash — proves the PeerID owner is the one in the Noise session |
| **Challenge-response** | Mutual nonce signing — defense-in-depth on top of Noise |
| **Message signatures** | Every envelope signed with Ed25519 — verified on receive |
| **TOFU key pinning** | Relay pins peer_id → public key on first registration |
| **Timestamped registration** | 60-second freshness window prevents replay of captured registrations |
| **Owner approval** | Both owners must approve — agents can't autonomously trust each other |
| **Message dedup** | 10K ring buffer rejects replayed message IDs |
| **Nonce guard** | Session terminates before AES-GCM nonce can overflow |

**What the relay can do**: see which PeerIDs are connected, when, and message sizes (traffic analysis).
**What the relay can't do**: read messages, forge messages, impersonate agents, or modify traffic without detection.

## API Reference

### `PeerIdentity`

```python
identity = PeerIdentity.generate()          # New random identity
identity = PeerIdentity.load(path)          # Load from ~/.p2p/identity/
identity.save(path, passphrase="optional")  # Save (optionally encrypted)
identity.peer_id                            # "p2p:4Tct..."
identity.sign(data) -> bytes                # Ed25519 signature
PeerIdentity.verify(data, sig, pubkey) -> bool
```

### `A2ANode`

```python
node = A2ANode(identity, peer_store, config,
               agent_name="Name", owner_display_name="Owner",
               capabilities=["list"], introduction="Description")

await node.start()                          # Begin listening + relay registration
await node.stop()                           # Graceful shutdown
await node.send(peer_id, msg_type, payload) # Send message (returns msg ID)
await node.connect_to_peer(peer_id)         # Initiate connection + handshake
await node.approve_handshake(peer_id, trust_level=1, scope_instructions="")
await node.reject_handshake(peer_id)
node.on_message(async_handler)              # Register message callback
node.on_approval_needed(async_handler)      # Register approval callback
node.health()                               # Returns status dict
```

### `PeerStore`

```python
store = PeerStore("~/.p2p/peers.json")
store.save(PeerRecord(peer_id="p2p:...", alias="Name", addresses=["ws://..."]))
store.get(peer_id) -> PeerRecord | None
store.list_all() -> list[PeerRecord]
store.remove(peer_id)
store.update_trust(peer_id, level)
store.update_description(peer_id, text)
```

## Requirements

- Python 3.11+
- Dependencies: `pydantic`, `cryptography`, `websockets`, `msgpack` (all well-maintained, widely used)

## License

MIT
