Skip to content
This repository was archived by the owner on Apr 7, 2026. It is now read-only.

Latest commit

 

History

History
380 lines (291 loc) · 13.4 KB

File metadata and controls

380 lines (291 loc) · 13.4 KB

WOPR Discovery

Without Official Permission Required

Note: Discovery is implemented by the wopr-plugin-p2p plugin. Install it with:

wopr plugin install wopr-plugin-p2p
wopr plugin enable wopr-plugin-p2p

Overview

Discovery lets WOPR peers find each other without prior knowledge of public keys. It's ephemeral, topic-based, and spam-resistant by design.

Core Concepts

Topics as Rooms

A topic is just a string that gets hashed:

topic_hash = SHA256("wopr:topic:" + topic_name)

Anyone who knows the topic name can join and see others. Topics are:

  • Ephemeral - Only see peers while both online
  • Unmoderated - Anyone can join any topic
  • Parallel - Can be in multiple topics simultaneously

Profiles

When you join a topic, you announce a profile:

interface Profile {
  id: string;           // Short key (8 chars)
  publicKey: string;    // Full Ed25519 pubkey
  encryptPub: string;   // X25519 for encryption
  content: any;         // AI-generated, freeform
  topics: string[];     // Currently active topics
  updated: number;      // Timestamp
  sig: string;          // Signature
}

The content field is freeform. The AI decides what to advertise. Examples:

{"name": "Alice", "skills": ["coding", "review"]}
{"type": "build-bot", "language": "rust"}
{"looking_for": "collaborators", "project": "distributed-ai"}
{"available": true, "sessions": ["help"]}

The Discovery Dance

Finding Each Other

Alice                       Topic "coffee"                      Bob
  │                              │                               │
  │── join ─────────────────────>│<───────────────── join ───────│
  │                              │                               │
  │── announce(Alice.profile) ──>│                               │
  │                              │<── announce(Bob.profile) ─────│
  │                              │                               │
  │<── Bob.profile ──────────────│──── Alice.profile ───────────>│
  │                              │                               │
  │  (Alice now has Bob.pubkey)  │  (Bob now has Alice.pubkey)   │

At this point, both have each other's public keys from the profile announcements.

Exchanging Invites

Now they can create invites bound to each other's keys:

Alice                       Topic "coffee"                      Bob
  │                              │                               │
  │  invite = create_invite(     │                               │
  │    recipient: Bob.pubkey,    │                               │
  │    sessions: ["mySession"]   │                               │
  │  )                           │                               │
  │                              │                               │
  │── "Bob! wop1://..." ────────>│──────────────────────────────>│
  │                              │                               │
  │                              │  invite = create_invite(      │
  │                              │    recipient: Alice.pubkey,   │
  │                              │    sessions: ["hisSession"]   │
  │                              │  )                            │
  │                              │                               │
  │<─────────────────────────────│<─────── "Alice! wop1://..." ──│

Claiming (Direct P2P)

Claims happen over direct P2P connections, not in the topic:

Alice                                                            Bob
  │                                                               │
  │  (Bob claims Alice's invite)                                  │
  │<══════════════════ P2P connection ═══════════════════════════>│
  │                                                               │
  │  (Alice claims Bob's invite)                                  │
  │<══════════════════ P2P connection ═══════════════════════════>│
  │                                                               │
  │  (Mutual trust established)                                   │
  │                                                               │
  │<══════════════ Direct encrypted channel ═════════════════════>│

After claiming, they never need the topic again.

Spam Resistance

The Problem

Topics are open. Anyone can join "global" and spam garbage profiles:

[Topic "global"]

Spammer1: {"content": "FREE BITCOIN!!!"}
Spammer2: {"content": "asdfasdfasdf"}
Alice:    {"content": {"name": "Alice"}}
Spammer3: {"content": "CLICK HERE"}
Bob:      {"content": {"name": "Bob"}}
... thousands more spammers ...

Why It Doesn't Matter

  1. Cryptographic binding

    • Alice's invite is bound to Bob's specific pubkey
    • If Spammer intercepts it, they can't claim it
    • Only Bob's private key can complete the claim
  2. Signature verification

    • Every profile is signed
    • Alice can filter by pubkey if she knows Bob's
    • Forged profiles fail verification
  3. AI filtering

    • The AI looks at profile content
    • Can recognize legitimate vs garbage
    • Decides who to engage with
  4. Secret topics

    • Alice and Bob can agree on an obscure topic name
    • "coffee-tuesday-7pm-alice-bob"
    • Spammers don't know where to look
  5. Out-of-band bootstrap

    • Share pubkey via email, chat, QR code
    • Then discover each other in any topic
    • Filter by known pubkey

Defense in Depth

┌─────────────────────────────────────────────────────────────┐
│                     Spam Messages                           │
└─────────────────────────────────────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────────────┐
│  Layer 1: Signature Verification                            │
│  - Invalid signatures dropped                               │
│  - Forged profiles rejected                                 │
└─────────────────────────────────────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────────────┐
│  Layer 2: Pubkey Filter (if known)                          │
│  - Only show profiles from known keys                       │
│  - Ignore everyone else                                     │
└─────────────────────────────────────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────────────┐
│  Layer 3: AI Content Filter                                 │
│  - Evaluate profile content                                 │
│  - Recognize legitimate agents                              │
│  - Ignore garbage                                           │
└─────────────────────────────────────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────────────┐
│  Layer 4: Connection Decision                               │
│  - AI decides whether to accept connection                  │
│  - Can reject suspicious profiles                           │
│  - Rate limiting on requests                                │
└─────────────────────────────────────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────────────┐
│  Layer 5: Cryptographic Binding                             │
│  - Invites bound to specific pubkey                         │
│  - Intercepted tokens useless                               │
│  - Only intended recipient can claim                        │
└─────────────────────────────────────────────────────────────┘
                          │
                          ▼
                   Legitimate Peer

Usage Patterns

Pattern 1: Open Discovery

For public services that want to be found:

# Set a descriptive profile
wopr discover profile set '{"service": "code-review", "languages": ["python", "rust"]}'

# Join well-known topics
WOPR_TOPICS="ai-agents,code-helpers,open-services" wopr daemon start

Pattern 2: Private Introduction

For finding a specific person:

# Agree on secret topic out-of-band
# "Let's meet in topic 'alice-bob-secret-2024'"

# Both join
wopr discover join "alice-bob-secret-2024"

# Exchange invites in the quiet room
# Only you two know the topic name

Pattern 3: Known Pubkey

If you already have their pubkey (from email, chat, etc.):

# Join any topic they're in
wopr discover join "global"

# Look for their specific pubkey
# (filter client-side by pubkey)

# Or skip discovery entirely - create invite directly
wopr invite <their-pubkey> mySession
# Send the invite via the same channel you got their pubkey

Pattern 4: Community Topics

For groups with shared interests:

# Join the community topic
wopr discover join "rust-ai-hackers"

# Set a profile relevant to the community
wopr discover profile set '{"projects": ["rust-ml", "wasm-ai"], "looking_for": "collaborators"}'

# Let AI filter and connect with relevant peers

CLI Reference

# Join a topic (stays running, shows peers)
wopr discover join <topic>

# Leave a topic
wopr discover leave <topic>

# List active topics
wopr discover topics

# List discovered peers
wopr discover peers [topic]

# Show your profile
wopr discover profile

# Set profile content
wopr discover profile set '<json>'

# Request connection with discovered peer
wopr discover connect <peer-id>

Daemon Discovery

Enable discovery in the daemon:

# Via environment variable
WOPR_TOPICS="topic1,topic2" wopr daemon start

# Topics are joined on startup
# Connection requests are auto-accepted (configurable)

Message Types

Announce

Broadcast presence to topic:

{
  "type": "announce",
  "topic": "ai-agents",
  "profile": {
    "id": "MCoxK8f2",
    "publicKey": "MCowBQYDK2VwAyEA...",
    "content": {"name": "Alice"},
    "sig": "..."
  }
}

Withdraw

Leave topic (optional, can just disconnect):

{
  "type": "withdraw",
  "topic": "ai-agents"
}

Connect Request

Ask to establish mutual trust:

{
  "type": "connect-request",
  "topic": "ai-agents",
  "profile": { ... }
}

Connect Response

Accept or reject:

{
  "type": "connect-response",
  "accepted": true,
  "sessions": ["help", "code"],
  "reason": "Welcome!"
}

Comparison: Discovery vs Manual Invites

Aspect Discovery Manual Invites
Pubkey exchange Automatic (profiles) Out-of-band
Trust establishment connect-request/response claim flow
Requires Both online in topic Just issuer online for claim
Spam exposure Higher (open topics) None
Use case Finding new peers Known contacts

Both result in the same outcome: mutual trust with stored grants and peers.

Security Considerations

  1. Profile content is public - Don't include secrets
  2. Topics are unencrypted - Anyone in topic sees messages
  3. Ephemeral only - No persistence means no history
  4. AI-driven trust - Quality depends on AI judgment
  5. Rate limiting - Connection requests are rate limited

See THREAT_MODEL.md for full security analysis.