retalk 0.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- retalk-0.0.1/PKG-INFO +381 -0
- retalk-0.0.1/README.md +372 -0
- retalk-0.0.1/pyproject.toml +18 -0
- retalk-0.0.1/src/retalk/__init__.py +6 -0
- retalk-0.0.1/src/retalk/cli.py +439 -0
- retalk-0.0.1/src/retalk/server.py +307 -0
- retalk-0.0.1/src/retalk/user.py +374 -0
retalk-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: retalk
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Minimal, self-hosted, end-to-end-encrypted messaging bus for AI agents, services, and humans
|
|
5
|
+
Author: Xing Han Lu
|
|
6
|
+
Requires-Dist: vodozemac
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
|
|
10
|
+
# retalk
|
|
11
|
+
|
|
12
|
+
Retalk is a small, self-hosted message bus for AI agents, services, and
|
|
13
|
+
people. Messages are end-to-end encrypted. The server only relays encrypted
|
|
14
|
+
blobs and publishes public keys.
|
|
15
|
+
|
|
16
|
+
The short version:
|
|
17
|
+
|
|
18
|
+
- The server never receives plaintext or private keys.
|
|
19
|
+
- Clients encrypt, decrypt, and sign every request.
|
|
20
|
+
- There are no accounts, passwords, registration flows, or bearer tokens.
|
|
21
|
+
- A user's ID is also the fingerprint clients use to verify that user's keys.
|
|
22
|
+
- The server still sees metadata: sender, recipient, timing, and message size.
|
|
23
|
+
|
|
24
|
+
Retalk uses `vodozemac` for Olm encryption. Everything else uses plain
|
|
25
|
+
HTTP+JSON and the Python standard library.
|
|
26
|
+
|
|
27
|
+
## Concepts
|
|
28
|
+
|
|
29
|
+
A **user** is one participant with a keypair and a mailbox. A user can be an
|
|
30
|
+
AI agent, a bot, a service, or a person at a terminal.
|
|
31
|
+
|
|
32
|
+
An **owner** is the person or organization that runs one or more users. The
|
|
33
|
+
protocol does not model owners yet. Today, the protocol only knows users.
|
|
34
|
+
|
|
35
|
+
A **user ID** is a 32-character sha256 fingerprint of the user's public keys.
|
|
36
|
+
That ID is both:
|
|
37
|
+
|
|
38
|
+
- the address other users send messages to, and
|
|
39
|
+
- the key pin clients use to reject substituted keys.
|
|
40
|
+
|
|
41
|
+
Share user IDs over a channel the server does not control, such as chat,
|
|
42
|
+
email, or in person. A hostile server cannot safely swap keys for an ID,
|
|
43
|
+
because clients recompute the fingerprint and refuse mismatches with
|
|
44
|
+
`PIN MISMATCH`.
|
|
45
|
+
|
|
46
|
+
Display names work differently:
|
|
47
|
+
|
|
48
|
+
- A user's self-chosen name is encrypted inside each message. The server does
|
|
49
|
+
not see it. Clients show it with a `~` prefix because it is not verified.
|
|
50
|
+
- A peer name is your local label for a user ID, added with
|
|
51
|
+
`retalk add bob <id>`. It stays on your machine and takes priority over the
|
|
52
|
+
sender's self-chosen `~name`.
|
|
53
|
+
|
|
54
|
+
## Install
|
|
55
|
+
|
|
56
|
+
```sh
|
|
57
|
+
uv add retalk
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
This installs the Python library:
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
from retalk import User
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
It also installs two commands:
|
|
67
|
+
|
|
68
|
+
- `retalk` - user CLI
|
|
69
|
+
- `retalk-server` - relay server
|
|
70
|
+
|
|
71
|
+
For a global CLI install, use:
|
|
72
|
+
|
|
73
|
+
```sh
|
|
74
|
+
uv tool install retalk
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
For one-off runs:
|
|
78
|
+
|
|
79
|
+
```sh
|
|
80
|
+
uvx retalk --help
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
<details>
|
|
84
|
+
<summary>Other install options</summary>
|
|
85
|
+
|
|
86
|
+
With pip:
|
|
87
|
+
|
|
88
|
+
```sh
|
|
89
|
+
pip install retalk
|
|
90
|
+
pipx install retalk
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
From the latest repository version:
|
|
94
|
+
|
|
95
|
+
```sh
|
|
96
|
+
uv add git+https://github.com/xhluca/retalk
|
|
97
|
+
pip install git+https://github.com/xhluca/retalk
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
From a development clone:
|
|
101
|
+
|
|
102
|
+
```sh
|
|
103
|
+
git clone https://github.com/xhluca/retalk
|
|
104
|
+
cd retalk
|
|
105
|
+
uv sync
|
|
106
|
+
uv run retalk --help
|
|
107
|
+
uv run python -m unittest discover -s tests
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Without uv, run `pip install -e .` inside the clone.
|
|
111
|
+
|
|
112
|
+
</details>
|
|
113
|
+
|
|
114
|
+
## Start a server
|
|
115
|
+
|
|
116
|
+
Run the relay on a public machine:
|
|
117
|
+
|
|
118
|
+
```sh
|
|
119
|
+
SERVER_PORT=8766 SERVER_AUDIENCE=https://server.example.com retalk-server
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
There is no server-side user setup. Users publish their own public keys when
|
|
123
|
+
they first send or receive.
|
|
124
|
+
|
|
125
|
+
`SERVER_AUDIENCE` must exactly match the URL users connect to. Request
|
|
126
|
+
signatures are bound to that URL, so a mismatch causes signature failures.
|
|
127
|
+
|
|
128
|
+
For internet use, put TLS in front of the relay. Example Caddy config:
|
|
129
|
+
|
|
130
|
+
```caddy
|
|
131
|
+
server.example.com {
|
|
132
|
+
reverse_proxy 127.0.0.1:8766
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Create a user
|
|
137
|
+
|
|
138
|
+
Run this once on each machine:
|
|
139
|
+
|
|
140
|
+
```sh
|
|
141
|
+
retalk init -u --name alice-1 --server https://server.example.com
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
`init` creates a local identity and prints the user ID. The private keys are
|
|
145
|
+
encrypted with a secret you choose. In scripts, set that secret with
|
|
146
|
+
`PICKLE_SECRET`; otherwise the CLI prompts for it.
|
|
147
|
+
|
|
148
|
+
Then exchange user IDs out-of-band and save your peer:
|
|
149
|
+
|
|
150
|
+
```sh
|
|
151
|
+
retalk add bob <bob-user-id>
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Common commands:
|
|
155
|
+
|
|
156
|
+
```sh
|
|
157
|
+
retalk id # print my user ID
|
|
158
|
+
retalk add bob <bob-user-id> # save a trusted local name
|
|
159
|
+
retalk send bob "hello" # send one encrypted message
|
|
160
|
+
retalk receive # drain my mailbox once
|
|
161
|
+
retalk receive --follow # keep polling and maintain keys
|
|
162
|
+
retalk receive --json # one JSON object per message
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Identity locations
|
|
166
|
+
|
|
167
|
+
Each identity lives in its own folder.
|
|
168
|
+
|
|
169
|
+
- `retalk init -u` creates `~/.local/share/retalk/default/`.
|
|
170
|
+
- `retalk init -u work` creates `~/.local/share/retalk/work/`.
|
|
171
|
+
- `retalk init ./alice` creates an identity at `./alice/`.
|
|
172
|
+
|
|
173
|
+
Every command finds its identity in this order:
|
|
174
|
+
|
|
175
|
+
1. `-s DIR`
|
|
176
|
+
2. `-u [NAME]`
|
|
177
|
+
3. `STORE` environment variable
|
|
178
|
+
4. user-level `default`, if it exists
|
|
179
|
+
|
|
180
|
+
Only `retalk init` creates an identity. Other commands fail if the selected
|
|
181
|
+
folder does not already contain one. Each acting command prints
|
|
182
|
+
`using <name> (<id>) from <dir>` to stderr so stdout stays clean for messages
|
|
183
|
+
and JSON.
|
|
184
|
+
|
|
185
|
+
Machines need a roughly correct clock. Server request signatures expire after
|
|
186
|
+
about 2.5 minutes.
|
|
187
|
+
|
|
188
|
+
## Two-minute local demo
|
|
189
|
+
|
|
190
|
+
This demo runs on one machine. It creates two identities and a local relay.
|
|
191
|
+
|
|
192
|
+
Terminal 1:
|
|
193
|
+
|
|
194
|
+
```sh
|
|
195
|
+
SERVER_AUDIENCE=http://127.0.0.1:8766 retalk-server
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Terminal 2:
|
|
199
|
+
|
|
200
|
+
```sh
|
|
201
|
+
export SERVER_URL=http://127.0.0.1:8766
|
|
202
|
+
|
|
203
|
+
ALICE_ID=$(PICKLE_SECRET=alice-secret retalk init ./alice --name alice)
|
|
204
|
+
BOB_ID=$(PICKLE_SECRET=bob-secret retalk init ./bob --name bob)
|
|
205
|
+
|
|
206
|
+
PICKLE_SECRET=alice-secret retalk add bob "$BOB_ID" -s ./alice
|
|
207
|
+
PICKLE_SECRET=bob-secret retalk add alice "$ALICE_ID" -s ./bob
|
|
208
|
+
|
|
209
|
+
PICKLE_SECRET=bob-secret retalk receive -s ./bob
|
|
210
|
+
PICKLE_SECRET=alice-secret retalk send bob "hello bob" -s ./alice
|
|
211
|
+
|
|
212
|
+
PICKLE_SECRET=bob-secret retalk receive -s ./bob
|
|
213
|
+
# alice: hello bob
|
|
214
|
+
|
|
215
|
+
PICKLE_SECRET=bob-secret retalk send alice "hi alice, got it" -s ./bob
|
|
216
|
+
PICKLE_SECRET=alice-secret retalk receive -s ./alice
|
|
217
|
+
# bob: hi alice, got it
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
The first `receive` publishes Bob's keys so Alice can start a session.
|
|
221
|
+
|
|
222
|
+
To inspect what the server stored:
|
|
223
|
+
|
|
224
|
+
```sh
|
|
225
|
+
sqlite3 server.db 'SELECT body FROM messages LIMIT 1'
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
You should see base64 ciphertext, not plaintext. Delivered messages are
|
|
229
|
+
deleted from the server.
|
|
230
|
+
|
|
231
|
+
## Two machines
|
|
232
|
+
|
|
233
|
+
Machine A:
|
|
234
|
+
|
|
235
|
+
```sh
|
|
236
|
+
retalk init -u --name alice --server https://server.example.com
|
|
237
|
+
# Share the printed user ID with Bob out-of-band.
|
|
238
|
+
|
|
239
|
+
retalk add bob <bob-user-id>
|
|
240
|
+
retalk send bob "hello from across the internet"
|
|
241
|
+
retalk receive --follow
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
Machine B does the same with Bob's identity and Alice's user ID.
|
|
245
|
+
|
|
246
|
+
After `init -u`, commands use the user-level identity by default, so you do
|
|
247
|
+
not need `-s` flags.
|
|
248
|
+
|
|
249
|
+
## Scripting
|
|
250
|
+
|
|
251
|
+
Drain the mailbox from cron:
|
|
252
|
+
|
|
253
|
+
```cron
|
|
254
|
+
*/5 * * * * PICKLE_SECRET=... retalk receive --json >> ~/inbox.jsonl 2>/dev/null
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
Pipe messages into another tool:
|
|
258
|
+
|
|
259
|
+
```sh
|
|
260
|
+
retalk receive --json | jq -r .text
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
Tiny auto-responder:
|
|
264
|
+
|
|
265
|
+
```sh
|
|
266
|
+
retalk receive --follow --json | while read -r msg; do
|
|
267
|
+
sender=$(jq -r .from <<<"$msg")
|
|
268
|
+
text=$(jq -r .text <<<"$msg")
|
|
269
|
+
retalk send "$sender" "you said: $text"
|
|
270
|
+
done
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
## Library usage
|
|
274
|
+
|
|
275
|
+
```python
|
|
276
|
+
from retalk import User
|
|
277
|
+
|
|
278
|
+
alice = User(
|
|
279
|
+
"https://server.example.com",
|
|
280
|
+
pickle_secret="...",
|
|
281
|
+
name="alice-1",
|
|
282
|
+
store="alice/store.db",
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
print(alice.user_id()) # share out-of-band
|
|
286
|
+
alice.publish() # publish public keys to this server
|
|
287
|
+
alice.send("<bob-user-id>", "hello")
|
|
288
|
+
|
|
289
|
+
for sender, name, text in alice.receive():
|
|
290
|
+
print(name or sender, text)
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
## Delivery
|
|
294
|
+
|
|
295
|
+
Each message carries an ID inside the encrypted envelope. When the recipient
|
|
296
|
+
decrypts it, the recipient sends back an encrypted acknowledgement.
|
|
297
|
+
|
|
298
|
+
Senders keep ciphertext in a local outbox until it is acknowledged.
|
|
299
|
+
`maintain()` resends messages that have gone unacknowledged for 2 minutes.
|
|
300
|
+
`retalk receive --follow` runs `maintain()` automatically.
|
|
301
|
+
|
|
302
|
+
This makes server loss or server migration recoverable:
|
|
303
|
+
|
|
304
|
+
- clients republish missing public keys,
|
|
305
|
+
- senders re-upload unacknowledged outbox messages, and
|
|
306
|
+
- recipients drop duplicate ciphertext that they have already processed.
|
|
307
|
+
|
|
308
|
+
## Key maintenance
|
|
309
|
+
|
|
310
|
+
Users publish one-time prekeys so peers can start encrypted sessions while
|
|
311
|
+
the user is offline.
|
|
312
|
+
|
|
313
|
+
`maintain()` keeps that server-side public key material healthy:
|
|
314
|
+
|
|
315
|
+
- it uploads 100 new one-time keys when fewer than 20 remain unclaimed,
|
|
316
|
+
- it rotates the reusable fallback key daily, and
|
|
317
|
+
- it resends unacknowledged outbox messages.
|
|
318
|
+
|
|
319
|
+
The fallback key is only used when the one-time key pool is empty. It keeps
|
|
320
|
+
new sessions available, but rotation limits how long the reusable key lives.
|
|
321
|
+
|
|
322
|
+
## More docs
|
|
323
|
+
|
|
324
|
+
- [docs/auth.md](docs/auth.md) explains signed requests, the exact wire
|
|
325
|
+
format, replay protection, and why retalk does not use bearer tokens.
|
|
326
|
+
- [docs/server.md](docs/server.md) explains what the relay stores, what
|
|
327
|
+
metadata it sees, why mailbox calls are authenticated, and what a hostile
|
|
328
|
+
server can and cannot do.
|
|
329
|
+
- [docs/olm.md](docs/olm.md) explains one-time prekeys, fallback keys,
|
|
330
|
+
replenishment, and rotation.
|
|
331
|
+
|
|
332
|
+
## Test
|
|
333
|
+
|
|
334
|
+
Run the full test suite from the repository root:
|
|
335
|
+
|
|
336
|
+
```sh
|
|
337
|
+
uv run python -m unittest discover -s tests -v
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
The tests use stdlib `unittest` and start their own local servers on ports
|
|
341
|
+
8767-8769. They keep all state in temporary directories and do not touch real
|
|
342
|
+
stores.
|
|
343
|
+
|
|
344
|
+
CI runs the same discovery on every push and pull request. See
|
|
345
|
+
[tests/README.md](tests/README.md).
|
|
346
|
+
|
|
347
|
+
Coverage includes:
|
|
348
|
+
|
|
349
|
+
- bidirectional encrypted delivery,
|
|
350
|
+
- no plaintext in the server database,
|
|
351
|
+
- delivered mail deletion,
|
|
352
|
+
- key substitution refusal with `PIN MISMATCH`,
|
|
353
|
+
- fallback-key session setup when one-time keys are drained,
|
|
354
|
+
- key replenishment and fallback rotation,
|
|
355
|
+
- in-flight messages across fallback rotation,
|
|
356
|
+
- concurrent sends from two processes sharing one store,
|
|
357
|
+
- migration to a fresh server,
|
|
358
|
+
- delivery acknowledgements and outbox recovery,
|
|
359
|
+
- duplicate rejection, and
|
|
360
|
+
- replayed, stale, and cross-server signed-request rejection.
|
|
361
|
+
|
|
362
|
+
## Release
|
|
363
|
+
|
|
364
|
+
Publishing is automated. Creating a GitHub Release triggers
|
|
365
|
+
`.github/workflows/publish.yaml`, which checks that the tag matches the
|
|
366
|
+
package version, runs the tests, builds with uv, and publishes to PyPI through
|
|
367
|
+
trusted publishing.
|
|
368
|
+
|
|
369
|
+
To cut a release:
|
|
370
|
+
|
|
371
|
+
1. Bump `version` in `pyproject.toml` and `src/retalk/__init__.py`.
|
|
372
|
+
2. Commit and push.
|
|
373
|
+
3. Create a release whose tag is the version, optionally prefixed with `v`.
|
|
374
|
+
|
|
375
|
+
```sh
|
|
376
|
+
gh release create v0.0.1 --title v0.0.1 --notes "first beta"
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
Maintainers only need to do PyPI setup once: on pypi.org, add a trusted
|
|
380
|
+
publisher for project `retalk` pointing at this repository, workflow
|
|
381
|
+
`publish.yaml`, environment `pypi`.
|