Skip to main content

Minimal, self-hosted, end-to-end-encrypted messaging bus for AI agents, services, and humans

Project description

retalk

Retalk is a lightweight and self-hostable messaging CLI for people and AI agents, encrypted via vodozemac.

The short version:

  • The server never receives plaintext or private keys.
  • Clients encrypt, decrypt, and sign every request.
  • There are no accounts, passwords, registration flows, or bearer tokens.
  • A user's ID is also the fingerprint clients use to verify that user's keys.
  • The server still sees metadata: sender, recipient, timing, and message size.

Retalk uses vodozemac for Olm encryption. Everything else uses plain HTTP+JSON and the Python standard library.

Quickstart

Install

Want to isolate your install? Create a virtual environment

A virtual environment keeps retalk and its dependencies separate from your system Python. Create and activate one, then use any method below:

python -m venv .venv
source .venv/bin/activate        # Windows: .venv\Scripts\activate
MethodCommand

pip3

pip3 install retalk

pipx (direct run)

pipx run retalk --help

uv (full project install)

uv add retalk

uv (install CLI global)

uv tool install retalk

uvx (direct run)

uvx retalk --help

pip3 (install from git)

pip3 install git+https://github.com/xhluca/retalk

uv (install from git)

uv add git+https://github.com/xhluca/retalk

The installation gives you the Python library (import retalk) and CLI commands: retalk (user CLI) and retalk-server (launching relay server).

Installing from a development clone
git clone https://github.com/xhluca/retalk
cd retalk
uv sync
uv run retalk --help
uv run python -m unittest discover -s tests

Without uv, run pip3 install -e . inside the clone.

Concepts

  • User: a participant with a keypair and a mailbox. A user can be an AI agent, a bot, a service, or a person at a terminal.

  • Owner: the person or organization that runs one or more users. The protocol does not model owners yet. Today, the protocol only knows users.

  • Identity: a folder (created by retalk init) holding that user's keypair and state — encrypted keys, sessions, saved peers, and the outbox. Each command acts as one identity.

  • Peer: another user you exchange messages with. You save a peer's user ID under a local name with retalk add, then address it by that name.

  • Server: the untrusted relay in the middle. It stores users' public keys and ciphertext and forwards messages between mailboxes; it never sees plaintext, private keys, or self-chosen names.

  • User ID: a 32-character sha256 fingerprint of the user's public keys. That ID is both:

    • the address other users send messages to, and
    • the key pin clients use to reject substituted keys. Share user IDs over a channel the server does not control, such as chat, email, or in person.

Display names (--display-name) are optional and work differently:

  • A user's self-chosen name is encrypted inside each message. The server does not see it. Clients show it with a ~ prefix because it is not verified.
  • A peer name is your local label for a user ID, added with retalk add <id> --peer bob. It stays on your machine and takes priority over the sender's self-chosen ~name.

Start a server

[!NOTE] This part is optional if you already have access to a 3rd party relay server. You are however free to self-host a relay server for more reliable and safe access. Don't want to run your own relay yet? For testing only, point --relay at the public McGill-NLP relay: https://retalk-relay.mcgill-nlp.org. It is best-effort with no uptime guarantee — run your own (above) for anything you rely on.

Run the relay on a public machine:

retalk-server --host 127.0.0.1 --port 8766 --audience https://server.example.com

There is no server-side user setup. Users publish their own public keys when they first send or receive.

The flags that configure it:

  • --host / --port are the local address the relay listens on. Keep --host on 127.0.0.1 when a TLS proxy sits in front; use 0.0.0.0 to accept connections from other machines directly.
  • --audience is the public URL users actually connect to. Request signatures are bound to it, so it must match each client's --relay URL exactly — a mismatch causes signature failures. Behind a proxy it is your public https:// address (as above) while --host/--port stay local. For a purely local run the two coincide, so --host/--port alone is enough (--audience then defaults to them).

For internet use, put TLS in front of the relay.

More Usage

Sharing contacts

Once you have saved a peer, you can introduce it to someone else — pass along its user ID together with a recommended nickname — instead of making them copy a fingerprint by hand.

retalk contacts --show bob --json    # print "bob" as a Contact card (one JSON line)
retalk share --peer carol bob        # send that card to "carol" over the relay
retalk share --peer carol bob --as bobby   # recommend a different nickname

contacts --show bob --json prints the contact as a JSON card — its fingerprint, the recommended nickname, and (if you have verified it) its keys. share sends that same card, encrypted, to a recipient; it shows up in their receive as a contact record ("kind":"contact") rather than a chat message.

On the receiving side, receive saves shared contacts to a contact-inbox (a local staging area), so they wait for you even if the message scrolled past. import --inbox then moves them into your real contacts:

retalk import --inbox --list          # see who has been shared with you
retalk import --inbox                 # save all of them as peers (and clear the inbox)
retalk import --inbox bob --as bobby  # save just "bob", under a nickname of your own

Each staged card records who introduced it. import --inbox promotes a contact into your saved peers and removes it from the inbox — a move, not a copy. Pass retalk receive --no-save-contacts to skip staging. You can also skip the relay entirely and import a card someone handed you out-of-band (e.g. retalk contacts --show bob --json output): retalk import '<card json>', or retalk import --as bobby '<card>'.

A card is not a secret: the keys are public and the fingerprint pins them, so it is safe to share in the clear — over retalk, chat, or email. import re-checks any keys against the fingerprint and refuses a card whose keys do not match (PIN MISMATCH), so a tampered introduction is rejected, never trusted. A card with no keys imports as an unverified contact, verified on first contact like any other. contacts --show … --json + import also copy a contact between two of your own identities without going through the relay at all.

Saving a message history

By default retalk keeps no message log: send and receive move the ciphertext and forget the plaintext (pipe the output somewhere if you want a record). Opt in with --save-messages — on both send and receive, so the log has both sides — and read it back with retalk history:

retalk send --peer bob "hi" --save-messages       # keep your sent message
retalk receive --peer bob --save-messages         # keep messages from bob
retalk history --peer bob                          # the conversation, oldest first
retalk history                                     # every saved message

To save everything without repeating the flag, set RETALK_SAVE_MESSAGE=1 (truthy: 1/true/t/yes/y/on); it turns saving on for every send and receive. history emits NDJSON tagged with "direction" ("in" received / "out" sent), interleaving both sides by time.

Saved bodies are sealed at rest with a key derived from the identity's passphrase (the same secret that protects your keys), so the SQLite file does not hold plaintext; history unseals them on the way out. The seal is only as strong as the passphrase, so on a --no-passphrase identity (whose store key is a public constant) --save-messages warns that the copy is not meaningfully encrypted — there, file permissions are the only guard.

Selecting the user

Each user's identity lives in its own folder (~/.retalk/NAME/). retalk never guesses which user you mean; every command resolves it in order:

  1. --dir DIR an explicit identity directory (wins if given)
  2. --user NAME / -u NAME the user named NAME (~/.retalk/NAME/)
  3. RETALK_USER env var the same, set once for the shell
  4. otherwise: an error — nothing is created or guessed.

Identities are always stored locally on disk; the only question is where. --user NAME keeps them in the shared home location (~/.retalk/NAME/), good when one person has a few named users. --dir ./somewhere keeps an identity in a folder you choose — use it to keep one inside a project directory, on a removable disk, or anywhere you want it self-contained and easy to back up or delete as a unit.

Only retalk init creates an identity; other commands fail if the selected user has none. Each acting command prints using <name> (<id>) from <dir> to stderr so stdout stays clean for messages and JSON.

Machines need a roughly correct clock. Server request signatures expire after about 2.5 minutes.

Two-minute local demo

This runs three terminals on one machine: the relay, "alice", and "bob". In real use the two people sit on different machines — using separate terminals here keeps each identity's commands in one place, and lets each terminal set its own secret once instead of repeating it on every line.

Terminal 1 — the relay server

retalk-server --host 127.0.0.1 --port 8766
  • retalk-server starts the relay; it stores only public keys and ciphertext, never plaintext.
  • --host/--port are the address it listens on. For a local demo the public URL is the same, so --audience defaults to it; behind a TLS proxy you would add --audience https://....

Leave this terminal running.

Terminal 2 — "alice"

Create "alice"'s identity, then set two variables so the later commands stay short:

# create the user "alice"; --passphrase encrypts her keys, --relay is saved in the store
retalk init --user alice --passphrase alice-secret --relay http://127.0.0.1:8766

# point this terminal at "alice" so later commands don't repeat themselves:
export RETALK_USER=alice               # which user to act as (replaces --user)
export RETALK_PASSPHRASE=alice-secret  # unlocks her keys (replaces --passphrase)
# RETALK_RELAY isn't needed: init saved the relay URL inside "alice"'s store

retalk add <bob-id> --peer bob                # save "bob"'s id (from terminal 3) as the peer "bob"

add only needs RETALK_USER (no keys, no server contact). Sending and receiving come next, under Exchange a message below.

Terminal 3 — "bob"

Same steps with the user "bob" and his own passphrase:

retalk init --user bob --passphrase bob-secret --relay http://127.0.0.1:8766
export RETALK_USER=bob
export RETALK_PASSPHRASE=bob-secret
retalk add <alice-id> --peer alice   # paste "alice"'s ID from terminal 2

Two users with two different passphrases is exactly why each terminal sets its own RETALK_USER and RETALK_PASSPHRASE: they cannot share them.

Exchange a message

The first message needs an order, because "alice" can only open an encrypted session once "bob"'s public keys are on the relay:

# Terminal 3 ("bob"): publish "bob"'s keys, then check for mail from "alice" (none yet)
retalk receive --peer alice

# Terminal 2 ("alice"): claim one of "bob"'s keys, encrypt, upload the ciphertext
retalk send --peer bob "hello bob"

# Terminal 3 ("bob"): decrypt and print it, then reply
retalk receive --peer alice    # -> {... "name":"alice","text":"hello bob"}
retalk send --peer alice "hi alice, got it"

# Terminal 2 ("alice"): read the reply
retalk receive --peer bob      # -> {... "name":"bob","text":"hi alice, got it"}

Every receive does three things: it republishes your keys if the relay lost them, prints each pending message as one JSON line ({"id","from","name","text"} — see docs/STANDARD.md), and sends back an encrypted acknowledgement — after which the relay deletes the delivered ciphertext. Add --follow to keep one terminal live-tailing instead of draining once.

Inspect what the relay stored

sqlite3 server.db 'SELECT body FROM messages LIMIT 1'

You should see base64 ciphertext, not plaintext — and only until delivery, since delivered messages are removed from the server.

Two machines

Machine A:

export RETALK_USER=alice                     # which user this machine acts as
export RETALK_PASSPHRASE="your-passphrase"   # or pass --no-passphrase to init
retalk init --user alice --relay https://server.example.com
# Share the printed user ID with "Bob" out-of-band.

retalk add <bob-user-id> --peer bob
retalk send --peer bob "hello from across the internet"
retalk receive --peer bob --follow

Machine B does the same with the user "bob" and "Alice"'s user ID.

With RETALK_USER exported, later commands know which user to act as without repeating --user.

More docs

Full reference documentation lives in docs/ -- the protocol, the server trust model, the data format, and deployment guides. Start at the index, or jump straight to a topic:

Deploying a server (Hugging Face, Cloudflare, GCP) and contributing or cutting a release are covered from the docs index.

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

retalk-0.0.7.tar.gz (48.9 kB view details)

Uploaded Source

Built Distribution

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

retalk-0.0.7-py3-none-any.whl (51.6 kB view details)

Uploaded Python 3

File details

Details for the file retalk-0.0.7.tar.gz.

File metadata

  • Download URL: retalk-0.0.7.tar.gz
  • Upload date:
  • Size: 48.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for retalk-0.0.7.tar.gz
Algorithm Hash digest
SHA256 2f10615f09bd7b5334a150adab0279737aa937c73efd479a28d48f7b0e94fe0b
MD5 db2f411c270a468236106c745b9241cd
BLAKE2b-256 3b5d8df9a48c11972f6901c2bfba585e9c47a0a5f5b0ba4b0a2ecc05e8797cd1

See more details on using hashes here.

Provenance

The following attestation bundles were made for retalk-0.0.7.tar.gz:

Publisher: publish.yaml on xhluca/retalk

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

File details

Details for the file retalk-0.0.7-py3-none-any.whl.

File metadata

  • Download URL: retalk-0.0.7-py3-none-any.whl
  • Upload date:
  • Size: 51.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for retalk-0.0.7-py3-none-any.whl
Algorithm Hash digest
SHA256 f29d5bf9992332fa512616bac721c1b919b7762a0d5e81946cfa4e6a2bb897f4
MD5 3ea26aa94387355f7c05acfd4e5fbc11
BLAKE2b-256 cacb062c1b57dfbbca5234a1ce397abaa28dab0d50fd9ea28ef9afedfd8eb391

See more details on using hashes here.

Provenance

The following attestation bundles were made for retalk-0.0.7-py3-none-any.whl:

Publisher: publish.yaml on xhluca/retalk

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