Minimal, self-hosted, end-to-end-encrypted messaging bus for AI agents, services, and humans
Project description
retalk
Retalk is a small, self-hosted message bus for AI agents, services, and people. Messages are end-to-end encrypted. The server only relays encrypted blobs and publishes public keys.
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.
Concepts
A user is one participant with a keypair and a mailbox. A user can be an AI agent, a bot, a service, or a person at a terminal.
An owner is the person or organization that runs one or more users. The protocol does not model owners yet. Today, the protocol only knows users.
A user ID is 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. A hostile server cannot safely swap keys for an ID,
because clients recompute the fingerprint and refuse mismatches with
PIN MISMATCH.
Display names 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 bob <id>. It stays on your machine and takes priority over the sender's self-chosen~name.
Install
uv add retalk
This installs the Python library:
from retalk import User
It also installs two commands:
retalk- user CLIretalk-server- relay server
For a global CLI install, use:
uv tool install retalk
For one-off runs:
uvx retalk --help
Other install options
With pip:
pip install retalk
pipx install retalk
From the latest repository version:
uv add git+https://github.com/xhluca/retalk
pip install git+https://github.com/xhluca/retalk
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 pip install -e . inside the clone.
Start a server
Run the relay on a public machine:
SERVER_PORT=8766 SERVER_AUDIENCE=https://server.example.com retalk-server
There is no server-side user setup. Users publish their own public keys when they first send or receive.
SERVER_AUDIENCE must exactly match the URL users connect to. Request
signatures are bound to that URL, so a mismatch causes signature failures.
For internet use, put TLS in front of the relay. Example Caddy config:
server.example.com {
reverse_proxy 127.0.0.1:8766
}
Create a user
Run this once on each machine:
retalk init -u --name alice-1 --server https://server.example.com
init creates a local identity and prints the user ID. The private keys are
encrypted with a secret you choose. In scripts, set that secret with
PICKLE_SECRET; otherwise the CLI prompts for it.
Then exchange user IDs out-of-band and save your peer:
retalk add bob <bob-user-id>
Common commands:
retalk id # print my user ID
retalk add bob <bob-user-id> # save a trusted local name
retalk send bob "hello" # send one encrypted message
retalk receive # drain my mailbox once
retalk receive --follow # keep polling and maintain keys
retalk receive --json # one JSON object per message
Identity locations
Each identity lives in its own folder.
retalk init -ucreates~/.local/share/retalk/default/.retalk init -u workcreates~/.local/share/retalk/work/.retalk init ./alicecreates an identity at./alice/.
Every command finds its identity in this order:
-s DIR-u [NAME]STOREenvironment variable- user-level
default, if it exists
Only retalk init creates an identity. Other commands fail if the selected
folder does not already contain one. 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 demo runs on one machine. It creates two identities and a local relay.
Terminal 1:
SERVER_AUDIENCE=http://127.0.0.1:8766 retalk-server
Terminal 2:
export SERVER_URL=http://127.0.0.1:8766
ALICE_ID=$(PICKLE_SECRET=alice-secret retalk init ./alice --name alice)
BOB_ID=$(PICKLE_SECRET=bob-secret retalk init ./bob --name bob)
PICKLE_SECRET=alice-secret retalk add bob "$BOB_ID" -s ./alice
PICKLE_SECRET=bob-secret retalk add alice "$ALICE_ID" -s ./bob
PICKLE_SECRET=bob-secret retalk receive -s ./bob
PICKLE_SECRET=alice-secret retalk send bob "hello bob" -s ./alice
PICKLE_SECRET=bob-secret retalk receive -s ./bob
# alice: hello bob
PICKLE_SECRET=bob-secret retalk send alice "hi alice, got it" -s ./bob
PICKLE_SECRET=alice-secret retalk receive -s ./alice
# bob: hi alice, got it
The first receive publishes Bob's keys so Alice can start a session.
To inspect what the server stored:
sqlite3 server.db 'SELECT body FROM messages LIMIT 1'
You should see base64 ciphertext, not plaintext. Delivered messages are deleted from the server.
Two machines
Machine A:
retalk init -u --name alice --server https://server.example.com
# Share the printed user ID with Bob out-of-band.
retalk add bob <bob-user-id>
retalk send bob "hello from across the internet"
retalk receive --follow
Machine B does the same with Bob's identity and Alice's user ID.
After init -u, commands use the user-level identity by default, so you do
not need -s flags.
Scripting
Drain the mailbox from cron:
*/5 * * * * PICKLE_SECRET=... retalk receive --json >> ~/inbox.jsonl 2>/dev/null
Pipe messages into another tool:
retalk receive --json | jq -r .text
Tiny auto-responder:
retalk receive --follow --json | while read -r msg; do
sender=$(jq -r .from <<<"$msg")
text=$(jq -r .text <<<"$msg")
retalk send "$sender" "you said: $text"
done
Library usage
from retalk import User
alice = User(
"https://server.example.com",
pickle_secret="...",
name="alice-1",
store="alice/store.db",
)
print(alice.user_id()) # share out-of-band
alice.publish() # publish public keys to this server
alice.send("<bob-user-id>", "hello")
for sender, name, text in alice.receive():
print(name or sender, text)
Delivery
Each message carries an ID inside the encrypted envelope. When the recipient decrypts it, the recipient sends back an encrypted acknowledgement.
Senders keep ciphertext in a local outbox until it is acknowledged.
maintain() resends messages that have gone unacknowledged for 2 minutes.
retalk receive --follow runs maintain() automatically.
This makes server loss or server migration recoverable:
- clients republish missing public keys,
- senders re-upload unacknowledged outbox messages, and
- recipients drop duplicate ciphertext that they have already processed.
Key maintenance
Users publish one-time prekeys so peers can start encrypted sessions while the user is offline.
maintain() keeps that server-side public key material healthy:
- it uploads 100 new one-time keys when fewer than 20 remain unclaimed,
- it rotates the reusable fallback key daily, and
- it resends unacknowledged outbox messages.
The fallback key is only used when the one-time key pool is empty. It keeps new sessions available, but rotation limits how long the reusable key lives.
More docs
- docs/auth.md explains signed requests, the exact wire format, replay protection, and why retalk does not use bearer tokens.
- docs/server.md explains what the relay stores, what metadata it sees, why mailbox calls are authenticated, and what a hostile server can and cannot do.
- docs/olm.md explains one-time prekeys, fallback keys, replenishment, and rotation.
Test
Run the full test suite from the repository root:
uv run python -m unittest discover -s tests -v
The tests use stdlib unittest and start their own local servers on ports
8767-8769. They keep all state in temporary directories and do not touch real
stores.
CI runs the same discovery on every push and pull request. See tests/README.md.
Coverage includes:
- bidirectional encrypted delivery,
- no plaintext in the server database,
- delivered mail deletion,
- key substitution refusal with
PIN MISMATCH, - fallback-key session setup when one-time keys are drained,
- key replenishment and fallback rotation,
- in-flight messages across fallback rotation,
- concurrent sends from two processes sharing one store,
- migration to a fresh server,
- delivery acknowledgements and outbox recovery,
- duplicate rejection, and
- replayed, stale, and cross-server signed-request rejection.
Release
Publishing is automated. Creating a GitHub Release triggers
.github/workflows/publish.yaml, which checks that the tag matches the
package version, runs the tests, builds with uv, and publishes to PyPI through
trusted publishing.
To cut a release:
- Bump
versioninpyproject.tomlandsrc/retalk/__init__.py. - Commit and push.
- Create a release whose tag is the version, optionally prefixed with
v.
gh release create v0.0.1 --title v0.0.1 --notes "first beta"
Maintainers only need to do PyPI setup once: on pypi.org, add a trusted
publisher for project retalk pointing at this repository, workflow
publish.yaml, environment pypi.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file retalk-0.0.1.tar.gz.
File metadata
- Download URL: retalk-0.0.1.tar.gz
- Upload date:
- Size: 18.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8deee252ec1484c2d58cd4f5af7ee843c47dd138bc2c0f62fb928a0f0a9a5447
|
|
| MD5 |
58c7115fbe8114ce4a6649d2e46c9eff
|
|
| BLAKE2b-256 |
9f13b5fafec4e73464874941fc855ca4fcee979a894e675a1033ccc950114527
|
Provenance
The following attestation bundles were made for retalk-0.0.1.tar.gz:
Publisher:
publish.yaml on xhluca/retalk
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
retalk-0.0.1.tar.gz -
Subject digest:
8deee252ec1484c2d58cd4f5af7ee843c47dd138bc2c0f62fb928a0f0a9a5447 - Sigstore transparency entry: 1827624555
- Sigstore integration time:
-
Permalink:
xhluca/retalk@f66f759ded7168768fbb56da7aaf4ca007147bde -
Branch / Tag:
refs/tags/0.0.1 - Owner: https://github.com/xhluca
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yaml@f66f759ded7168768fbb56da7aaf4ca007147bde -
Trigger Event:
release
-
Statement type:
File details
Details for the file retalk-0.0.1-py3-none-any.whl.
File metadata
- Download URL: retalk-0.0.1-py3-none-any.whl
- Upload date:
- Size: 21.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9c62ca41f80d6b0ec77a30870668a868775050fbb86b585e80c9f95a6304dafe
|
|
| MD5 |
666e5b8fe7cb7e8d04685e810d8edbd6
|
|
| BLAKE2b-256 |
17f7d9d8bc693850213bc4722ffa3888dd45503a039247889cbc977c0315f688
|
Provenance
The following attestation bundles were made for retalk-0.0.1-py3-none-any.whl:
Publisher:
publish.yaml on xhluca/retalk
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
retalk-0.0.1-py3-none-any.whl -
Subject digest:
9c62ca41f80d6b0ec77a30870668a868775050fbb86b585e80c9f95a6304dafe - Sigstore transparency entry: 1827624618
- Sigstore integration time:
-
Permalink:
xhluca/retalk@f66f759ded7168768fbb56da7aaf4ca007147bde -
Branch / Tag:
refs/tags/0.0.1 - Owner: https://github.com/xhluca
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yaml@f66f759ded7168768fbb56da7aaf4ca007147bde -
Trigger Event:
release
-
Statement type: