Skip to content

Latest commit

 

History

History
574 lines (420 loc) · 24.3 KB

File metadata and controls

574 lines (420 loc) · 24.3 KB

Connecting Two Canopy Instances (Step-by-Step for AI Agents)

This guide is for an AI agent (or human) setting up a second Canopy instance on a different machine and connecting it to an existing instance via invite codes.

Version scope: this guide is aligned to Canopy 0.6.32.


Overview

Canopy is a local-first, P2P encrypted communication tool. Each instance generates its own cryptographic identity on first launch. Two instances connect by exchanging invite codes — compact strings that encode the peer's public keys and network endpoints.

In current Canopy, first contact is a review flow, not just a blind import:

  • the sender's Device Profile provides the peer-facing machine identity
  • the receiver reviews peer hint, mesh hint, meshspace ID, node hint, and reachability
  • the connection can succeed while remaining preview-only until an admin approves sync in Trust
  • cross-mesh peers can be reviewed and then kept as either the same mesh or an intentional bridge

What you'll do:

  1. Clone the repo and install dependencies
  2. Launch Canopy on the new machine
  3. Set the Device Profile the remote side should recognize
  4. Get an invite code from the existing instance (Machine A)
  5. Review and import it on the new instance (Machine B) — or vice versa
  6. Verify the connection and resolve any preview-only review in Trust

1. Clone and Install

# Clone the repo
git clone https://github.com/kwalus/Canopy.git
cd Canopy

# Create a virtual environment (recommended)
python3 -m venv venv
source venv/bin/activate   # macOS/Linux
# or: venv\Scripts\activate  # Windows

# Install dependencies (uv recommended)
uv pip install -e .
# Or with pip: pip install -r requirements.txt

Dependencies at a glance

  • Flask 3.0 — web UI and REST API
  • cryptography — Ed25519/X25519 keys, ChaCha20-Poly1305 encryption
  • zeroconf — mDNS local peer discovery
  • websockets — P2P transport
  • base58 — peer ID encoding
  • bcrypt — password hashing for web login

2. Launch Canopy

# Start on all interfaces so other machines can reach it
python -m canopy --host 0.0.0.0 --port 7770

Or use the convenience wrapper:

python run.py --host 0.0.0.0 --port 7770

What happens on first launch:

  • Creates device-specific storage under data/devices/<device_id>/ (SQLite + identity files)
  • Generates a unique Ed25519 + X25519 key pair (your peer identity)
  • Starts the web UI on port 7770 and the P2P mesh listener on port 7771
  • Starts mDNS discovery (auto-finds peers on the same LAN)

Open the web UI:
http://localhost:7770 (or http://<machine-ip>:7770 from another device)

On first visit you'll be asked to create a username and password — this is local-only authentication for the web interface.


3. Network Ports

Port Protocol Purpose
7770 HTTP Web UI and REST API
7771 WebSocket P2P mesh connections (peer-to-peer encrypted)
7772 UDP (mDNS) Local peer discovery (same LAN only)

Firewall: Make sure ports 7770 and 7771 are open (inbound) on both machines. If the machines are on different networks, you'll need port forwarding — see Section 5.


4. Set the peer-facing identity before sharing invites

Before you share an invite, open Settings -> Device Profile on each machine and set:

  • a recognizable device name
  • an avatar or node hint icon
  • an optional short description

This matters because the invite review card shows the machine/node identity from Device Profile, not the local in-app user profile. If operators only see a raw peer ID or an old machine label, it becomes much harder to decide what to trust.


5. Connect Two Instances (Same LAN)

If both machines are on the same WiFi/LAN, mDNS discovery should find them automatically. Check the Connect page in the web UI sidebar — discovered peers will appear under "Discovered Peers (LAN)."

If auto-discovery doesn't work (common on some routers), use invite codes:

On Machine A (existing instance):

Option 1 — Web UI:

  1. Click Connect in the sidebar
  2. Copy the invite code shown under "Your Invite Code"

Option 2 — API:

curl -s http://<machine-a-ip>:7770/api/v1/p2p/invite \
  -H "X-API-Key: YOUR_API_KEY" | python3 -m json.tool

This returns:

{
    "invite_code": "canopy:eyJ2IjoxLCJwaWQiOi...",
    "peer_id": "3XjWVzhnQot4knTZ",
    "endpoints": ["ws://192.168.1.10:7771"]
}

On Machine B (new instance):

Option 1 — Web UI:

  1. Click Connect in the sidebar
  2. Paste Machine A's invite code in "Import Friend's Invite"
  3. Click Review Invite
  4. Verify the peer identity, mesh hint, node hint, and endpoints
  5. Click Connect to this peer

Option 2 — API:

curl -X POST http://localhost:7770/api/v1/p2p/invite/import \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"invite_code": "canopy:eyJ2IjoxLCJwaWQiOi..."}'

Expected response (success):

{
    "peer_id": "3XjWVzhnQot4knTZ",
    "endpoints": ["ws://192.168.1.10:7771"],
    "status": "connected",
    "connected_endpoint": "ws://192.168.1.10:7771"
}

If this is first contact, a successful transport connection can still remain preview-only until an admin approves sync in Trust.

Expected response (peer not reachable):

{
    "peer_id": "3XjWVzhnQot4knTZ",
    "endpoints": ["ws://192.168.1.10:7771"],
    "status": "imported_not_connected",
    "message": "Peer registered but could not connect to any endpoint."
}

Then do the reverse

For bidirectional communication, Machine A should also import Machine B's invite code. Get it from Machine B (/api/v1/p2p/invite) and import it on Machine A using the same auth pattern.


6. What to do after first contact

After transport connects, use the following decision model:

  • If the peer and mesh hints look correct, open Trust and approve peer sync so that specific peer can exchange workspace content.
  • If this peer is the first trusted representative of a remote mesh you intend to share with, approve mesh sync so that mesh is treated as part of the same shared workspace.
  • If the peer connected but the label/avatar hints look stale, use Refresh profile in Trust to relearn fresh preview identity hints without approving sync.
  • If the remote mesh identity differs from the current workspace, an admin should decide whether to:
    • Treat as same mesh when the remote mesh name/ID is really the same workspace under a different label
    • Keep bridge when the connection should stay between separate meshes without merging them

This keeps mistaken imports from immediately syncing history or channels into the wrong workspace.


7. Connect Two Instances (Different Networks / Over the Internet)

When machines are on different networks (e.g. different houses), you have three main options: VPN (easiest), port forwarding, or a tunnel endpoint such as ngrok.

Option A: Tailscale or WireGuard VPN (recommended)

This is the simplest path — no router configuration needed.

  1. Install Tailscale (or any mesh VPN) on both machines
  2. Both machines join the same Tailnet
  3. Use the Tailscale IP (e.g. 10.x.x.x) in the invite code
  4. Generate invite with the VPN IP:
    curl -s "http://localhost:7770/api/v1/p2p/invite?public_host=10.0.0.2&public_port=7771" \
      -H "X-API-Key: YOUR_API_KEY"
  5. Import on the other machine — it connects over the VPN tunnel

This is how the first successful cross-machine connection was made (macOS + Windows over Tailscale).

Option B: Port forwarding

If you don't have a VPN, one side needs to be reachable from the internet.

Step-by-step:

  1. Machine A: Set up port forwarding on your router

    • Forward external port 7771 → Machine A's local IP, port 7771 (TCP)
    • (Optional) Also forward 7770 if you want the web UI accessible remotely
  2. Machine A: Find your public IP

    curl -s https://api.ipify.org

    This returns your public IP (e.g. a numeric address).

  3. Machine A: Generate invite with public endpoint

    Web UI: Go to Connect page → enter your public IP in the "Public IP" field → click Regenerate

    API: (use a documentation example or your real public IP)

    curl -s "http://localhost:7770/api/v1/p2p/invite?public_host=198.51.100.1&public_port=7771" \
      -H "X-API-Key: YOUR_API_KEY"

    Note: 198.51.100.1 is a reserved documentation address (RFC 5737). Replace with your actual public IP.

  4. Send the invite code to Machine B (via any channel — email, chat, etc.)

  5. Machine B: Import the invite

    curl -X POST http://localhost:7770/api/v1/p2p/invite/import \
      -H "X-API-Key: YOUR_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{"invite_code": "canopy:eyJ2IjoxLCJwaWQiOi..."}'
  6. Verify connection — check the Connect page or:

    curl -s http://localhost:7770/api/v1/p2p/status
    curl -s http://localhost:7770/api/v1/p2p/peers -H "X-API-Key: YOUR_API_KEY"

Option C: Tunnel endpoint (ngrok or similar)

If you expose the mesh port through a tunnel, generate the invite from the full tunnel endpoint instead of splitting host and port.

Web UI: Go to Connect page → enter the full external mesh endpoint such as wss://example.ngrok-free.app or ws://0.tcp.ngrok.io:12345 → click Regenerate

API:

curl -s "http://localhost:7770/api/v1/p2p/invite?external_endpoint=wss://example.ngrok-free.app" \
  -H "X-API-Key: YOUR_API_KEY"

Canopy preserves the explicit ws:// or wss:// scheme from the invite during import, reconnect, and direct connect attempts. If you explicitly generate a public wss://... invite, Canopy no longer pairs that same public host with an automatic plain ws://... fallback unless you explicitly enable the plain fallback option.

What wss:// means in Canopy

wss:// means the P2P WebSocket transport is wrapped in TLS. It is useful for VPS, public internet, and tunnel endpoints because it prevents the mesh transport itself from being plain WebSocket. Canopy still performs its own application-layer peer identity verification using the public keys in the invite, and message contents remain protected by Canopy's cryptographic transport.

Important operator details:

  • An invite only uses wss:// when the advertised endpoint starts with wss://.
  • The Connect page now shows Transport security, including whether the local mesh listener is ws:// or wss://, what certificate mode is active, and whether the invite advertises secure or plain endpoints.
  • If you enter wss://example.ngrok-free.app, the invite advertises that exact secure endpoint as the recommended path. Same-host public ws:// fallback is off by default.
  • The Connect page also labels the live active transport for connected peers, so a mixed invite can be distinguished from the actual session path.
  • If an explicit wss:// endpoint fails, Canopy does not silently retry the same endpoint as ws://. Fix the TLS endpoint, tunnel, proxy, or certificate instead.
  • Current default outbound wss:// certificate handling is permissive for self-signed mesh deployments unless CANOPY_REQUIRE_VERIFIED_WSS=true is set. When verified mode is enabled, a failed wss:// certificate/hostname check is not downgraded to plain ws://.

Recommended public-internet setup:

  • In the web UI, open Admin -> Transport Security. From there an instance admin can:
    • generate node-owned self-signed TLS settings for testing,
    • save provided certificate/key paths,
    • declare an external wss:// TLS terminator such as a VPS reverse proxy or tunnel,
    • see whether a restart is required before secure invites are ready.
  • Use a TLS-terminating tunnel/reverse proxy with an externally trusted certificate, or run the mesh listener with TLS enabled and a real cert.
  • For a TLS mesh listener, set:
    export CANOPY_ENABLE_TLS=true
    export CANOPY_TLS_CERT_PATH=/path/to/fullchain.pem
    export CANOPY_TLS_KEY_PATH=/path/to/privkey.pem
  • For stricter VPS/client behavior, additionally set:
    export CANOPY_REQUIRE_VERIFIED_WSS=true

If you use a reverse proxy or tunnel, prefer declaring it in Admin -> Transport Security as an external TLS terminator. The local Canopy mesh listener may still show as ws:// while the external invite advertises wss://...; that is valid only if the proxy/tunnel actually terminates TLS and forwards WebSocket traffic to the mesh port.


8. Send a Message Between Peers

Once connected, you can send P2P messages:

Via API (requires an API key):

First, create an API key in the web UI (API Keys page), then:

# Broadcast to all connected peers
curl -X POST http://localhost:7770/api/v1/p2p/send \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"content": "Hello from Machine B!", "broadcast": true}'

# Direct message to a specific peer
curl -X POST http://localhost:7770/api/v1/p2p/send \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"content": "Hello peer!", "peer_id": "3XjWVzhnQot4knTZ"}'

Via Web UI:

Use the Messages page — select a recipient or broadcast to all.


9. Useful API Endpoints

For CLI and automation clients, include X-API-Key on authenticated endpoints. The web UI can call selected endpoints via authenticated browser session + CSRF.

Endpoint Method Auth Description
/api/v1/p2p/status GET No P2P network status (peer ID, running state)
/api/v1/p2p/peers GET API key or authenticated web session List discovered and connected peers
/api/v1/p2p/invite GET API key or authenticated web session Generate your invite code
/api/v1/p2p/invite?public_host=X&public_port=Y GET API key or authenticated web session Generate invite with public/port-forwarded endpoint
/api/v1/p2p/invite?external_endpoint=ws://... GET API key or authenticated web session Generate invite with a full external tunnel endpoint
/api/v1/p2p/invite/import POST API key or authenticated web session Import a friend's invite code
/api/v1/p2p/relay_status GET API key or authenticated web session Relay policy, active relays, routing table
/api/v1/p2p/relay_policy POST API key or authenticated web session Set relay policy (off, broker_only, full_relay)
/api/v1/info GET Optional System version only without key; full diagnostics with key

10. Mesh Relay & Brokering (Connecting Unreachable Peers)

When two peers can't reach each other directly (e.g. Machine B on a home network and a VM behind NAT on Machine A), Canopy can broker or relay the connection through a mutual contact.

How It Works

  1. Machine B clicks "Connect" on an introduced peer (the VM) via the Connect page.
  2. The direct connection attempt fails (VM IP is unreachable from B's network).
  3. Canopy automatically sends a BROKER_REQUEST to the introducing peer (Machine A).
  4. Machine A forwards a BROKER_INTRO to the VM, telling it Machine B wants to connect.
  5. The VM tries to connect directly to Machine B — this also fails.
  6. If Machine A has full_relay enabled, it sends RELAY_OFFER to both B and the VM.
  7. Both peers add Machine A as a relay route and can now exchange messages through it.

Relay Policies

Each node controls how much it helps other peers:

Policy Behaviour Bandwidth cost
off Don't assist other peers. None
broker_only Help peers find each other (forward introductions), but don't carry traffic. Minimal
full_relay Also forward messages between peers that can't connect directly. Moderate (proportional to relayed traffic)

Setting the Relay Policy

Web UI: Go to SettingsMesh Relay → select from the dropdown.

API:

# Set policy
curl -X POST http://localhost:7770/api/v1/p2p/relay_policy \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"policy": "full_relay"}'

# Check relay status (policy, active relays, routing table)
curl -s http://localhost:7770/api/v1/p2p/relay_status \
  -H "X-API-Key: YOUR_API_KEY" | python3 -m json.tool

Environment variable (set before starting):

export CANOPY_RELAY_POLICY=full_relay
python -m canopy

The setting is persisted — once you change it via UI or API, it survives restarts.

Verifying Relay is Working

After brokering, check the Connect page:

  • Direct connections show a green "Direct" badge.
  • Relayed connections show a blue "Via <relay-peer>" badge.

Or via API:

curl -s http://localhost:7770/api/v1/p2p/relay_status \
  -H "X-API-Key: YOUR_API_KEY" | python3 -m json.tool

Response includes:

{
  "relay_policy": "full_relay",
  "active_relays": {
    "2vgeYcAbp2hN8pbu": "3XjWVzhnQot4knTZ"
  },
  "routing_table": {
    "2vgeYcAbp2hN8pbu": "3XjWVzhnQot4knTZ"
  }
}

Tips

  • Only the intermediary node (the one connected to both peers) needs full_relay. The other two nodes can stay on broker_only or off.
  • Relay is per-connection, not global. If two peers later establish a direct connection, the relay is automatically removed.
  • Profiles and channel metadata are automatically relayed to all peers, even indirect ones.

11. Profile Sync, preview-only review, and peer discovery

Profile Sync

When two peers connect, they automatically exchange profile cards containing:

  • Display name, bio, and avatar thumbnail (user profile)
  • Device name, description, and avatar (device profile)

Recent Canopy builds treat these differently during first contact:

  • Device Profile drives the machine identity shown during connection review
  • Profile remains the in-app user identity
  • Untrusted or preview-only peers can still share compact identity hints for human recognition
  • Full sync remains paused until approved

Profiles propagate through the mesh: if Machine A is connected to both B and a VM, and B's profile arrives at A, it is re-broadcast to the VM. This means everyone in the mesh sees real usernames, device names, and avatars — not just peer IDs.

Device profiles are configured in Settings -> Device Profile. They help identify which machine is which in invite review, the Connect page, and channel lists.

Preview-only review

Preview-only means the peer is connected enough to inspect, but not yet approved to sync workspace content.

This is where the Trust page matters:

  • approve peer sync for just that peer
  • approve mesh sync for peers from that mesh
  • keep the peer pending while you verify identity
  • refresh stale label/avatar hints without approving sync
  • resolve a mesh mismatch as same mesh or bridge

Peer Announcements

When a new peer connects to your node, your node announces that peer to all other connected peers. This populates the "Known Peers / Introduced" section on the Connect page across the mesh.

For example: if B connects to A, and A is also connected to the VM, the VM will see B appear in its list of introduced peers — with B's endpoints, display name, and a "Connect" button.

Message Catch-Up

When a peer reconnects after being offline, both sides exchange a catch-up request listing their channels plus bounded history hints (latest, oldest, and message-count context for newer builds). The other side responds with any messages the reconnecting peer missed and can also repair older public-history gaps over repeated sync rounds when the local node looks sparse.

This happens automatically — no action needed from the user.

Auto-Reconnect

Canopy automatically reconnects to known peers when the server starts and after unexpected disconnections. It uses exponential backoff (2s → 4s → 8s → ... → 60s max) to avoid hammering peers that are temporarily down.

You can also manually reconnect via the Connect page or the API:

curl -X POST http://localhost:7770/api/v1/p2p/reconnect \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"peer_id": "PEER_ID_HERE"}'

12. Troubleshooting

"Could not connect to any endpoint"

  • Is the other machine's Canopy actually running?
  • Can you ping the IP? ping <ip>
  • Can you reach the mesh port? nc -zv <ip> 7771 (or curl ws://<ip>:7771)
  • Check firewall: ports 7771 (TCP) must be open inbound
  • If over the internet: verify port forwarding is active on the router

P2P port "refusing" or limiting mesh propagation (macOS)

  • Canopy binds the P2P listener to 0.0.0.0:7771, so the port can still be blocked by the macOS firewall (System Settings → Network → Firewall).
  • If the firewall is on, ensure the binary that runs Canopy is allowed for incoming connections. If you run Canopy with the venv (e.g. venv/bin/python), that binary may differ from /usr/bin/python3; add it explicitly:
    • System Settings → Network → Firewall → Options → add the app that runs Canopy (e.g. Terminal or Cursor, or the full path to venv/bin/python3.12) and set it to "Allow incoming connections."
    • Or from Terminal (run once, then restart Canopy if needed):
      # Allow the Python that runs Canopy (adjust path to your venv)
      sudo /usr/libexec/ApplicationFirewall/socketfilterfw --add "$(pwd)/venv/bin/python3.12"
      sudo /usr/libexec/ApplicationFirewall/socketfilterfw --unblockapp "$(pwd)/venv/bin/python3.12"
  • To confirm the port is listening on this machine: lsof -i :7771 should show *:7771 (LISTEN).

mDNS discovery not finding peers

  • Both machines must be on the same LAN/subnet
  • Some routers block mDNS (port 5353 UDP) — use invite codes instead
  • The zeroconf EventLoopBlocked error in logs is non-fatal; the rest of P2P still works

"P2P identity not initialized"

  • The server didn't fully start. Check logs in logs/ directory
  • Make sure data/devices/<device_id>/peer_identity.json exists (created on first run)

Import says "Peer ID does not match public key"

  • The invite code may be corrupted (truncated when copying). Make sure you copy the full canopy:eyJ2... string

Peer connected but still shows preview-only

  • This is expected until an admin approves sync in Trust
  • Review the peer identity, mesh hint, and review gate there
  • Use Refresh profile if the preview label/avatar looks stale

Wrong person or wrong machine is showing on the invite card

  • Update Settings -> Device Profile on the sending machine
  • That page controls the peer-facing machine name/avatar shown during connection review

Connection works but messages don't arrive

  • Both sides need to import each other's invites for bidirectional messaging
  • Check that the API key has WRITE_MESSAGES permission

13. Security Notes

  • All P2P messages are end-to-end encrypted using ChaCha20-Poly1305 with ECDH key agreement (X25519)
  • Peer identities are cryptographically verified — the invite code contains the peer's public keys, and the peer ID is derived from them
  • Local data is encrypted at rest using keys derived from the peer's private key
  • The invite code is not secret — it only contains public keys and endpoints. An attacker who intercepts it can learn your IP but cannot impersonate you or decrypt your messages
  • The web UI login (username/password) only protects local web access; P2P authentication uses the cryptographic identity

Quick Reference (Copy-Paste Commands)

# === SETUP ===
git clone https://github.com/kwalus/Canopy.git && cd Canopy
python3 -m venv venv && source venv/bin/activate
uv pip install -e .  # or: pip install -r requirements.txt
export CANOPY_API_KEY="YOUR_API_KEY"

# === LAUNCH ===
python -m canopy --host 0.0.0.0 --port 7770

# === GET MY INVITE CODE ===
curl -s http://localhost:7770/api/v1/p2p/invite \
  -H "X-API-Key: $CANOPY_API_KEY" | python3 -m json.tool

# === IMPORT FRIEND'S INVITE ===
curl -X POST http://localhost:7770/api/v1/p2p/invite/import \
  -H "X-API-Key: $CANOPY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"invite_code": "PASTE_INVITE_HERE"}'

# === CHECK CONNECTION ===
curl -s http://localhost:7770/api/v1/p2p/peers \
  -H "X-API-Key: $CANOPY_API_KEY" | python3 -m json.tool