Skip to main content

Minimal inbox-only messaging for agents

Project description

deaddrop

Minimal inbox-only messaging for agents.

Installation

pip install deaddrop

# With Turso support for production
pip install deaddrop[turso]

Concepts

  • Namespace: An isolated group of mailboxes. ID is derived from a secret (ns_id = hash(ns_secret)[:16]).
  • Identity/Mailbox: An agent's inbox within a namespace. ID is derived from a secret (id = hash(secret)[:16]).
  • Message: A blob sent from one identity to another within a namespace. Uses UUIDv7 (timestamp-sortable).
  • Room: A shared space for multi-user group messaging. Any member can read/write. See docs/ROOMS.md.

Auth Model

Three tiers of authentication, with clear separation of concerns:

Role Namespaces Mailboxes Messages
Admin CRUD CRUD (metadata only) ❌ None
Namespace Owner Archive own CRUD (metadata only) ❌ None
Mailbox Owner List peers Own inbox only

Key principle: Neither admin nor namespace owner can read message contents. Only the mailbox owner can access their inbox.

Self-messaging: Mailbox owners can send messages to themselves. This enables use cases like notes-to-self, scheduled reminders, or persisting state between sessions.

Message Lifecycle

UNREAD (∞) → READ (TTL starts) → EXPIRED → DELETED
         ↘ ARCHIVED (preserved) ↙
  • Unread: Message lives indefinitely until read
  • Read: TTL countdown starts (default: 24 hours, configurable per-namespace)
  • Archived: User-preserved messages (no expiration)
  • Expired: Automatically deleted by TTL job

Sender can also set TTL for ephemeral messages that expire from creation time (instead of read time).

Web App

Deaddrop includes a web-based messaging client for human users.

Invite System

Admins can generate single-use invite links for human users:

# Create an invite link (expires in 24h by default)
deadrop invite create {ns} {identity_id} --name "Agent Human"

# The command outputs a shareable URL like:
# https://your-server.com/join/abc123def456#base64urlkey

Security: The invite uses AES-256-GCM encryption:

  • The URL fragment (#key) contains the decryption key and is never sent to the server
  • The server stores only the encrypted secret (cannot decrypt without the key)
  • Invites are single-use and can optionally expire

Web App Routes

Route Description
/ Landing page
/join/{invite_id} Claim an invite link
/app Dashboard (list stored namespaces)
/app/{slug} Inbox view for a namespace
/app/{slug}/{peer_id} Conversation with a specific peer
/app/{slug}/archived Archived messages

Credential Storage

The web app stores credentials in localStorage:

  • Persists across browser sessions
  • Supports multiple namespaces and identities per namespace
  • Users can switch between identities within a namespace
  • Credentials never sent to server (only used for API auth headers)

Namespace Slugs

Set human-readable URLs for namespaces:

# Instead of /app/abc123def456, use /app/project-alpha
deadrop ns set-slug {ns} project-alpha

Python Library

Deaddrop provides a unified Python API that works with local, remote, and in-memory backends.

Basic Usage

from deadrop import Deaddrop

# Auto-discover backend (local .deaddrop or remote config)
client = Deaddrop()

# Or use explicit backends
client = Deaddrop.local()              # Local .deaddrop directory
client = Deaddrop.remote(url="...")    # Remote server
client = Deaddrop.in_memory()          # Ephemeral (testing)
client = Deaddrop.create_local()       # Create new .deaddrop

Full Workflow

from deadrop import Deaddrop

# Create or open local deaddrop
client = Deaddrop.create_local()

# Create namespace and identities
ns = client.create_namespace(display_name="My Project")
alice = client.create_identity(ns["ns"], display_name="Alice")
bob = client.create_identity(ns["ns"], display_name="Bob")

# Send message
client.send_message(
    ns=ns["ns"],
    from_secret=alice["secret"],
    to_id=bob["id"],
    body="Hello Bob!"
)

# Read inbox
messages = client.get_inbox(
    ns=ns["ns"],
    identity_id=bob["id"],
    secret=bob["secret"]
)

for msg in messages:
    print(f"From: {msg['from']}, Body: {msg['body']}")

Rooms (Group Messaging)

from deadrop import Deaddrop

client = Deaddrop.in_memory()

# Setup
ns = client.create_namespace(display_name="Team")
alice = client.create_identity(ns["ns"], display_name="Alice")
bob = client.create_identity(ns["ns"], display_name="Bob")

# Alice creates a room and invites Bob
room = client.create_room(ns["ns"], alice["secret"], "Project Chat")
client.add_room_member(ns["ns"], room["room_id"], bob["id"], alice["secret"])

# Both can send messages
client.send_room_message(ns["ns"], room["room_id"], alice["secret"], "Hello team!")
client.send_room_message(ns["ns"], room["room_id"], bob["secret"], "Hey Alice!")

# Both can read all messages
messages = client.get_room_messages(ns["ns"], room["room_id"], bob["secret"])
for msg in messages:
    print(f"{msg['from_id']}: {msg['body']}")

# Long-polling for real-time updates
for msg in client.listen_room(ns["ns"], room["room_id"], bob["secret"]):
    print(f"New: {msg['body']}")

See docs/ROOMS.md for the complete rooms guide.

Testing

import pytest
from deadrop import Deaddrop

# Use in-memory backend for fast tests
@pytest.fixture
def client():
    return Deaddrop.in_memory()

def test_agent_messaging(client):
    setup = client.quick_setup("Test", ["Alice", "Bob"])
    
    client.send_message(
        setup["namespace"]["ns"],
        setup["identities"]["Alice"]["secret"],
        setup["identities"]["Bob"]["id"],
        "Hello!"
    )
    
    messages = client.get_inbox(
        setup["namespace"]["ns"],
        setup["identities"]["Bob"]["id"],
        setup["identities"]["Bob"]["secret"]
    )
    
    assert len(messages) == 1

See docs/LOCAL_NAMESPACES.md, docs/ROOMS.md, and docs/TESTING.md for detailed guides.

Quick Start

1. Start the Server

# Development mode (no auth required)
deadrop serve --no-auth

# With auto-reload
deadrop serve --no-auth --reload

2. Configure the CLI

# Interactive wizard
deadrop init

# Or show current config
deadrop config

3. Create Resources

# Create a namespace
deadrop ns create --display-name "My Project"

# Create identities (mailboxes)
deadrop identity create {ns} --display-name "Agent 1"
deadrop identity create {ns} --display-name "Agent 2"

# Send a message
deadrop message send {ns} {recipient_id} "Hello!"

# Read inbox
deadrop message inbox {ns}

Running the Server

Development Mode

# No authentication required for admin endpoints
deadrop serve --no-auth

Production Mode

Deaddrop supports pluggable authentication. Configure one of:

# Option 1: Custom auth module (recommended)
export DEADROP_AUTH_MODULE=myapp.auth
deadrop serve

# Option 2: heare-auth service
export HEARE_AUTH_URL=https://your-auth-service.com
deadrop serve

# Option 3: Legacy static token
export DEADROP_ADMIN_TOKEN=your-secret-token
deadrop serve

Custom Auth Module

Create a Python module that exposes:

# myapp/auth.py

from deaddrop.auth_provider import AuthResult

def is_enabled() -> bool:
    """Return True if auth is configured."""
    return True

def verify_bearer_token(token: str) -> AuthResult:
    """Verify a bearer token and return auth result."""
    # Your verification logic here
    if valid:
        return AuthResult(valid=True, key_id="...", name="...", metadata={})
    return AuthResult(valid=False, error="Invalid token")

def extract_bearer_token(authorization: str | None) -> str | None:
    """Optional: Custom token extraction from Authorization header."""
    # Default implementation handles "Bearer <token>" format
    ...

Storage

Local (default): SQLite file

export DEADROP_DB=deadrop.db

Production: Turso (SQLite at the edge)

export TURSO_URL=libsql://your-db.turso.io
export TURSO_AUTH_TOKEN=your-token
pip install deaddrop[turso]

API

Admin Endpoints

Requires bearer token authentication (or --no-auth mode).

POST /admin/namespaces              # Create namespace
GET /admin/namespaces               # List namespaces
DELETE /admin/namespaces/{ns}       # Delete namespace

Namespace Owner Endpoints

Requires X-Namespace-Secret header.

POST /{ns}/archive                  # Archive namespace
POST /{ns}/identities               # Create identity
GET /{ns}/identities                # List identities
DELETE /{ns}/identities/{id}        # Delete identity

Mailbox Owner Endpoints

Requires X-Inbox-Secret header.

# List peers
GET /{ns}/identities

# Send message
POST /{ns}/send
{"to": "recipient_id", "body": "Hello!"}
{"to": "recipient_id", "body": "Ephemeral!", "ttl_hours": 1}  # Expires from creation

# Read inbox (marks as read, starts TTL)
GET /{ns}/inbox/{id}
GET /{ns}/inbox/{id}?unread=true        # Only unread
GET /{ns}/inbox/{id}?after={mid}        # Cursor pagination
GET /{ns}/inbox/{id}?wait=30            # Long-poll for 30 seconds

# Archive/unarchive message
POST /{ns}/inbox/{id}/{mid}/archive
POST /{ns}/inbox/{id}/{mid}/unarchive
GET /{ns}/inbox/{id}/archived           # List archived messages

# Delete message immediately
DELETE /{ns}/inbox/{id}/{mid}

Invite Endpoints

# Get invite info (no auth required)
GET /api/invites/{invite_id}/info

# Claim invite (no auth required, single-use)
POST /api/invites/{invite_id}/claim

# Create invite (requires X-Namespace-Secret)
POST /{ns}/invites

# List invites (requires X-Namespace-Secret)
GET /{ns}/invites

# Revoke invite (requires X-Namespace-Secret)
DELETE /{ns}/invites/{invite_id}

Room Endpoints

Requires X-Inbox-Secret header (must be a room member for most operations).

# Create room
POST /{ns}/rooms
{"display_name": "Project Chat"}

# List rooms I'm a member of
GET /{ns}/rooms

# Get room details
GET /{ns}/rooms/{room_id}

# Delete room (requires X-Namespace-Secret)
DELETE /{ns}/rooms/{room_id}

# List members
GET /{ns}/rooms/{room_id}/members

# Add member
POST /{ns}/rooms/{room_id}/members
{"identity_id": "member_to_add"}

# Remove member
DELETE /{ns}/rooms/{room_id}/members/{identity_id}

# Send message
POST /{ns}/rooms/{room_id}/messages
{"body": "Hello!", "content_type": "text/plain"}

# Get messages (with long-polling)
GET /{ns}/rooms/{room_id}/messages
GET /{ns}/rooms/{room_id}/messages?after={mid}&wait=30

# Update read cursor
POST /{ns}/rooms/{room_id}/read
{"last_read_mid": "message_id"}

# Get unread count
GET /{ns}/rooms/{room_id}/unread

Environment Variables

Server

Variable Description
DEADROP_NO_AUTH Set to 1 for development (no admin auth)
DEADROP_AUTH_MODULE Python module path for custom auth
HEARE_AUTH_URL URL of heare-auth service (built-in)
DEADROP_ADMIN_TOKEN Legacy static admin token
DEADROP_DB SQLite database path (default: deadrop.db)
TURSO_URL Turso database URL
TURSO_AUTH_TOKEN Turso authentication token

CLI

The CLI uses ~/.config/deadrop/config.yaml for configuration. Run deadrop init to set up interactively.

Deployment

Docker

FROM python:3.11-slim
RUN pip install deaddrop[turso]
CMD ["deadrop", "serve"]

Dokku

# Create app
dokku apps:create deaddrop

# Set environment
dokku config:set deaddrop DEADROP_AUTH_MODULE=myapp.auth
dokku config:set deaddrop TURSO_URL=libsql://your-db.turso.io
dokku config:set deaddrop TURSO_AUTH_TOKEN=your-turso-token

# Deploy
git push dokku main

Long-Polling

Deaddrop supports long-polling for efficient real-time message delivery without constant polling.

API Usage

Add the wait query parameter (1-60 seconds) to the inbox endpoint:

# Wait up to 30 seconds for new messages
GET /{ns}/inbox/{id}?wait=30

# Combine with other parameters
GET /{ns}/inbox/{id}?wait=30&unread=true
GET /{ns}/inbox/{id}?wait=30&after={mid}

Behavior:

  • If messages exist, returns immediately
  • If no messages, holds connection open until messages arrive or timeout
  • Returns same response format as regular inbox endpoint
  • Server polls internally every 500ms

Python Library

from deadrop import Deaddrop

client = Deaddrop.in_memory()  # or .local() or .remote()
setup = client.quick_setup("Test", ["Alice", "Bob"])

# Single long-poll call
messages = client.get_inbox(
    setup["namespace"]["ns"],
    setup["identities"]["Bob"]["id"],
    setup["identities"]["Bob"]["secret"],
    wait=30  # Wait up to 30 seconds
)

# Convenience method
messages = client.wait_for_messages(
    setup["namespace"]["ns"],
    setup["identities"]["Bob"]["id"],
    setup["identities"]["Bob"]["secret"],
    timeout=30
)

# Generator for continuous listening
for msg in client.listen(ns, bob_id, bob_secret, timeout=30):
    print(f"Received: {msg['body']}")
    if should_stop():
        break

Security Notes

  • Secret-derived IDs: Can't claim an identity without the secret
  • No plaintext secrets stored: Server only stores hashes
  • Namespace isolation: Agents only interact within their namespace
  • Content privacy: Admin/namespace owners cannot read messages
  • Config file security: Namespace YAML files contain secrets - protect them!

Known Limitations

  • No end-to-end encryption (encrypt your own payloads)
  • No message signing (recipient trusts from field)
  • No rate limiting (yet)
  • Replay attacks possible (use TTLs and nonces)

CLI Reference

# Configuration
deadrop init                    # Setup wizard
deadrop config                  # Show current config

# Namespaces
deadrop ns create               # Create namespace
deadrop ns create --ttl-hours 1 # Custom TTL (hours after read)
deadrop ns list                 # List local namespaces
deadrop ns list --remote        # List from server
deadrop ns show {ns}            # Show details
deadrop ns secret {ns}          # Show namespace secret
deadrop ns archive {ns}         # Archive namespace
deadrop ns delete {ns}          # Delete local config
deadrop ns delete {ns} --remote # Delete from server

# Identities
deadrop identity create {ns}    # Create identity
deadrop identity list {ns}      # List local identities
deadrop identity show {ns} {id} # Show details
deadrop identity export {ns} {id}           # Export for handoff
deadrop identity export {ns} {id} --format json
deadrop identity export {ns} {id} --format env
deadrop identity delete {ns} {id}
deadrop identity delete {ns} {id} --remote

# Messages (for testing)
deadrop message send {ns} {to} "Hello!"
deadrop message send {ns} {to} "Hi" --identity-id {from}
deadrop message send {ns} {my_id} "Note to self"  # Self-message
deadrop message inbox {ns}                  # Read all
deadrop message inbox {ns} --unread         # Only unread
deadrop message inbox {ns} --after {mid}    # After cursor
deadrop message delete {ns} {mid}           # Delete immediately

# Invites (for web app access)
deadrop invite create {ns} {identity_id}    # Create invite link
deadrop invite create {ns} {id} --name "Name" --expires-in 48h
deadrop invite list {ns}                    # List pending invites
deadrop invite revoke {ns} {invite_id}      # Revoke an invite

# Server
deadrop serve                   # Run server
deadrop serve --no-auth         # Development mode
deadrop serve --reload          # With auto-reload

# Jobs (requires DB access)
deadrop jobs ttl                # Process expired messages
deadrop jobs ttl --dry-run      # Show what would be processed
deadrop jobs ttl --archive-path /path/to/archives

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

deaddrop-0.4.4.tar.gz (193.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

deaddrop-0.4.4-py3-none-any.whl (84.7 kB view details)

Uploaded Python 3

File details

Details for the file deaddrop-0.4.4.tar.gz.

File metadata

  • Download URL: deaddrop-0.4.4.tar.gz
  • Upload date:
  • Size: 193.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for deaddrop-0.4.4.tar.gz
Algorithm Hash digest
SHA256 5ec412c0a2f6ffdd09ced5402bbb28252211126c937c41c8b15dafb1153343a2
MD5 c316b6e8af308e5a3625bab0301df451
BLAKE2b-256 0565db61fa5daffa2b436817473c2a6962d8e86a073f9d5e86f9ab7ae716077b

See more details on using hashes here.

Provenance

The following attestation bundles were made for deaddrop-0.4.4.tar.gz:

Publisher: release.yml on clusterfudge/deaddrop

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file deaddrop-0.4.4-py3-none-any.whl.

File metadata

  • Download URL: deaddrop-0.4.4-py3-none-any.whl
  • Upload date:
  • Size: 84.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for deaddrop-0.4.4-py3-none-any.whl
Algorithm Hash digest
SHA256 b1559fea45aa266bccce2c0ed1e109090cf8eef5158b64a2dde8f28846e20f9d
MD5 a82a8b488533ff615296dceef1ed42b8
BLAKE2b-256 ffc4afd814d460d8b391f12ec03dbbeb083bfb6b59450891a3a9c604dfd9e550

See more details on using hashes here.

Provenance

The following attestation bundles were made for deaddrop-0.4.4-py3-none-any.whl:

Publisher: release.yml on clusterfudge/deaddrop

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page