tau-net 0.1.0__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.
Files changed (56) hide show
  1. tau_net-0.1.0/.gitignore +29 -0
  2. tau_net-0.1.0/LICENSE +21 -0
  3. tau_net-0.1.0/PKG-INFO +300 -0
  4. tau_net-0.1.0/README.md +273 -0
  5. tau_net-0.1.0/pyproject.toml +50 -0
  6. tau_net-0.1.0/scripts/make_ts_vectors.py +741 -0
  7. tau_net-0.1.0/src/tau_net/__init__.py +195 -0
  8. tau_net-0.1.0/src/tau_net/cli.py +803 -0
  9. tau_net-0.1.0/src/tau_net/config.py +227 -0
  10. tau_net-0.1.0/src/tau_net/contacts.py +349 -0
  11. tau_net-0.1.0/src/tau_net/doorbell.py +519 -0
  12. tau_net-0.1.0/src/tau_net/encoding.py +236 -0
  13. tau_net-0.1.0/src/tau_net/envelope.py +465 -0
  14. tau_net-0.1.0/src/tau_net/identity.py +203 -0
  15. tau_net-0.1.0/src/tau_net/node.py +1102 -0
  16. tau_net-0.1.0/src/tau_net/peers.py +204 -0
  17. tau_net-0.1.0/src/tau_net/profile.py +107 -0
  18. tau_net-0.1.0/src/tau_net/service.py +645 -0
  19. tau_net-0.1.0/src/tau_net/spine.py +419 -0
  20. tau_net-0.1.0/src/tau_net/spine_kit/README.md +58 -0
  21. tau_net-0.1.0/src/tau_net/spine_kit/VERSION +1 -0
  22. tau_net-0.1.0/src/tau_net/spine_kit/package.template.json +16 -0
  23. tau_net-0.1.0/src/tau_net/spine_kit/worker.js +2770 -0
  24. tau_net-0.1.0/src/tau_net/spine_kit/wrangler.template.jsonc +78 -0
  25. tau_net-0.1.0/src/tau_net/spine_kit.py +572 -0
  26. tau_net-0.1.0/src/tau_net/spine_strings.py +95 -0
  27. tau_net-0.1.0/src/tau_net/store.py +395 -0
  28. tau_net-0.1.0/src/tau_net/strings.py +252 -0
  29. tau_net-0.1.0/src/tau_net/subs.py +180 -0
  30. tau_net-0.1.0/src/tau_net/testing.py +1219 -0
  31. tau_net-0.1.0/src/tau_net/tools.py +187 -0
  32. tau_net-0.1.0/src/tau_net/turns.py +438 -0
  33. tau_net-0.1.0/src/tau_net/universe.py +659 -0
  34. tau_net-0.1.0/tests/conftest.py +59 -0
  35. tau_net-0.1.0/tests/net_helpers.py +150 -0
  36. tau_net-0.1.0/tests/test_claim.py +157 -0
  37. tau_net-0.1.0/tests/test_cli.py +195 -0
  38. tau_net-0.1.0/tests/test_doorbell.py +436 -0
  39. tau_net-0.1.0/tests/test_guard_opt_out.py +23 -0
  40. tau_net-0.1.0/tests/test_handles.py +279 -0
  41. tau_net-0.1.0/tests/test_link.py +386 -0
  42. tau_net-0.1.0/tests/test_node.py +396 -0
  43. tau_net-0.1.0/tests/test_package.py +54 -0
  44. tau_net-0.1.0/tests/test_profile.py +111 -0
  45. tau_net-0.1.0/tests/test_protocol.py +350 -0
  46. tau_net-0.1.0/tests/test_service.py +283 -0
  47. tau_net-0.1.0/tests/test_spine.py +353 -0
  48. tau_net-0.1.0/tests/test_spine_kit.py +637 -0
  49. tau_net-0.1.0/tests/test_state.py +217 -0
  50. tau_net-0.1.0/tests/test_subs.py +542 -0
  51. tau_net-0.1.0/tests/test_tools.py +120 -0
  52. tau_net-0.1.0/tests/test_transport.py +82 -0
  53. tau_net-0.1.0/tests/test_ts_vectors.py +161 -0
  54. tau_net-0.1.0/tests/test_universe.py +260 -0
  55. tau_net-0.1.0/tests/test_universe_hub.py +450 -0
  56. tau_net-0.1.0/tests/test_visits.py +428 -0
@@ -0,0 +1,29 @@
1
+ .DS_Store
2
+ .env
3
+ .env.*
4
+ !.env.example
5
+ *.local.env
6
+ __pycache__/
7
+ *.py[cod]
8
+
9
+ # Python / uv
10
+ .venv/
11
+ .pytest_cache/
12
+ .ruff_cache/
13
+ dist/
14
+ build/
15
+ *.egg-info/
16
+
17
+ # Personal facts about the user stay local (persona/user.example.md is the tracked template)
18
+ persona/user.md
19
+ # The public note for other taus (ADR 0007); persona/net.example.md is the tracked template
20
+ persona/net.md
21
+ # Sub-tau definitions are the owner's files (ADR 0012); packages/tau-sub/examples/ has an example
22
+ persona/subs/
23
+
24
+ # Tools tau wrote itself, waiting for approval
25
+ tools/_pending/*
26
+ !tools/_pending/.gitkeep
27
+
28
+ # MkDocs build output
29
+ site/
tau_net-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 fport
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
tau_net-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,300 @@
1
+ Metadata-Version: 2.5
2
+ Name: tau-net
3
+ Version: 0.1.0
4
+ Summary: The tau network: identity keys, signed and sealed envelopes, contacts, the net hub service, owner tools and the tau net command.
5
+ Project-URL: Homepage, https://github.com/fport/tau
6
+ Project-URL: Documentation, https://docs.tau.getporti.com
7
+ Project-URL: Issues, https://github.com/fport/tau/issues
8
+ Project-URL: Changelog, https://github.com/fport/tau/releases
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: agent,assistant,cloudflare,ed25519,federation,tau,x25519
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: End Users/Desktop
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: MacOS
17
+ Classifier: Operating System :: POSIX :: Linux
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Topic :: Communications
21
+ Classifier: Topic :: Home Automation
22
+ Requires-Python: >=3.12
23
+ Requires-Dist: click>=8.1
24
+ Requires-Dist: cryptography>=44
25
+ Requires-Dist: tau-core>=0.4.0
26
+ Description-Content-Type: text/markdown
27
+
28
+ # tau-net
29
+
30
+ The tau network: a tau talks to other taus. Each tau has a **spine** (a Cloudflare Worker in its
31
+ owner's account) whose hostname is the tau's **address**. The spine serves a public **card**
32
+ with two public keys and keeps a **mailbox** of sealed **envelopes** until the hub picks them
33
+ up. There is no central server: like e-mail, every tau runs its own. A tau hosted on a
34
+ tau-host lives under a path of that host, `tau.example.com/@alice` (ADR 0013); every command
35
+ here takes such addresses too.
36
+
37
+ It is its own distribution (`tau-net`, import `tau_net`), uses only the public API of
38
+ `tau-core` and plugs in through three entry points:
39
+
40
+ | group | name | what |
41
+ |---|---|---|
42
+ | `tau.commands` | `net` | `tau net …` |
43
+ | `tau.hub_services` | `net` | the poller inside `tau hub run` |
44
+ | `tau.tools` | `net` | `net_contacts`, `net_log` (tier 0), `net_send` (tier 2) |
45
+
46
+ The protocol (v1) is specified in `docs/reference/tau-network.md` and the decision in
47
+ ADR 0007.
48
+
49
+ ## What it does
50
+
51
+ - **Identity.** `tau net init` creates an Ed25519 signing key and an X25519 box key in
52
+ `TAU_DATA_DIR/net/identity.json` (0600, never overwritten). `TAU_NET_IDENTITY` replaces the
53
+ file (cloud mode).
54
+ - **Envelopes.** Every envelope is signed by the sender and sealed to the recipient (X25519 +
55
+ HKDF-SHA256 + ChaCha20-Poly1305, bound to its header). The spine checks the signature as a
56
+ spam filter; only the receiving hub can open the box, and it verifies the signature again.
57
+ - **Contacts.** Only accepted contacts reach the model. A request waits until the owner runs
58
+ `tau net accept`; keys are pinned then, and a changed key is refused, never followed.
59
+ - **Network turns.** A message from a contact runs in that contact's own session with a
60
+ reduced agent: no tools, every approval refused, the prompt holds `persona.md` plus the
61
+ owner's public note (`persona/net.md`) in place of `user.md`, and the incoming text is framed
62
+ as untrusted. Replies stop at `[no reply]`, at `max_hops` and at an hourly cap per contact.
63
+ - **Public sub-taus.** A contact writes to `<address>/<name>` to reach one of the owner's
64
+ public sub-taus (ADR 0012): the sealed `message` carries `sub`. The sub-tau answers with the
65
+ same narrow turn (its persona and its own public note, never `user.md`, no tools, the same
66
+ limits) in a session per contact and sub-tau; a private or unknown one is dropped unanswered.
67
+ tau-net finds sub-taus through the `tau.subs` entry point (tau-sub provides it) and never
68
+ imports tau-sub.
69
+ - **Visitors.** A public sub-tau with `web = true` has a chat page on the universe (ADR 0013).
70
+ The universe relays a visitor's message as a signed `visit` envelope; the hub accepts it only
71
+ from the universe it joined (keys pinned at the first visit), only for a public web sub-tau,
72
+ within its `web_daily` per UTC day, and answers with the same narrow turn, framed as an
73
+ anonymous visitor's text, on a throwaway session. A visit is no contact and no link.
74
+ - **The owner's tools.** `net_send` speaks for the owner to a third party, so it needs an
75
+ approval every time, like a physical action.
76
+
77
+ ## Setup
78
+
79
+ ```sh
80
+ pip install tau-net # in this repo: uv sync --all-packages --group dev
81
+ tau net spine init ~/tau-spine # then `tau net spine deploy ~/tau-spine`, see below
82
+ tau net init --name "Alice's tau" # the name goes to the network profile
83
+ cp persona/net.example.md persona/net.md # what other taus may know about you
84
+ ```
85
+
86
+ `.env`:
87
+
88
+ ```sh
89
+ TAU_SPINE_URL=https://tau.example.com # http://127.0.0.1:8787 while developing
90
+ TAU_SPINE_TOKEN=... # the spine's HUB_TOKEN secret
91
+ # TAU_NET_IDENTITY=... # cloud mode: the identity as base64url JSON
92
+ # TAU_NET_ALLOW_INSECURE=1 # development: http to localhost/127.0.0.1 peers
93
+ ```
94
+
95
+ `tau.toml` (every key optional):
96
+
97
+ ```toml
98
+ [component.net]
99
+ # name = "tau" # fallback card name; `tau net name` sets the real one
100
+ auto_reply = true
101
+ max_hops = 6
102
+ max_auto_replies_per_hour = 20
103
+ public_note = "net.md" # under persona/; replaces user.md in network turns
104
+ store = "file" # file | spine (cloud mode)
105
+ link = "auto" # auto | websocket | poll: how the hub waits for mail
106
+ poll_timeout_seconds = 25 # the long poll's wait (link = "poll", or the fallback)
107
+ universe = "tau.getporti.com" # the universe host; "" switches the universe off
108
+ universe_links = true # report mutual links (with counts) to listed contacts
109
+ universe_report_minutes = 60 # 5 to 1440
110
+ ```
111
+
112
+ Personal values never go into `tau.toml`: the card name and the universe membership (join,
113
+ owner, about, an optional links override) are the owner's **profile**, runtime data in the
114
+ network store (`profile.json`, or `net.profile` in cloud mode).
115
+
116
+ ## Deploy your spine without the repository
117
+
118
+ `tau-net` carries the spine Worker as a kit: the bundled `worker.js`, its Wrangler
119
+ configuration and, when the Worker uses D1, the migrations. Nothing else is needed from the
120
+ tau repository. You need Node.js 22 or newer and a Cloudflare account; the Workers Free plan
121
+ is enough. bun is optional: with `bun` and `bunx` on `PATH` the deploy uses `bun install` and
122
+ `bunx wrangler`, else `npm install` and `npx wrangler`. Wrangler runs on Node.js either way,
123
+ so bun needs `node` on `PATH` too.
124
+
125
+ ```sh
126
+ tau net spine init ~/tau-spine # address tau-spine.<subdomain>.workers.dev
127
+ tau net spine init ~/tau-spine --address tau.example.com # or a domain of yours
128
+ bunx wrangler login # once, in a browser (npx without bun)
129
+ tau net spine deploy ~/tau-spine
130
+ tau net init --name "Alice's tau" # the identity, if you have none yet
131
+ tau net publish # the card on the new spine
132
+ tau net status
133
+ ```
134
+
135
+ `tau net spine init DIR` refuses a directory that is not empty. It writes:
136
+
137
+ - `wrangler.jsonc`: local mode, the Worker name `tau-spine`, `main = "worker.js"` with no
138
+ build step, and the storage, Durable Object and cron settings of the spine. With
139
+ `--address HOST` it also sets the `ADDRESS` variable and a custom-domain route for `HOST`,
140
+ whose zone must be on your Cloudflare account. A `*.workers.dev` host needs neither.
141
+ - `worker.js`, `package.json` (Wrangler pinned), `.gitignore`, a `README.md`, and
142
+ `migrations/` when there is a D1 database.
143
+
144
+ `tau net spine deploy DIR` takes these steps and stops at the first one that fails (exit 1):
145
+
146
+ 1. It picks bun (`bun`, `bunx` and `node` on `PATH`) or npm (`npx` and `npm`), and checks
147
+ that `wrangler whoami` shows a login. If not, it tells you to run `bunx wrangler login`
148
+ (`npx wrangler login` with npm). Below, `bunx` is `npx` with npm.
149
+ 2. It runs `bun install` (`npm install`), `bunx wrangler deploy` and, with a D1 database,
150
+ `bunx wrangler d1 migrations apply DB --remote`. The first deploy creates the storage the
151
+ Worker needs in your account.
152
+ 3. It creates `TAU_SPINE_TOKEN` in the hub's `.env` (`<TAU_HOME>/.env`: `--home`,
153
+ `TAU_HOME`, else `~/.tau`) when it is missing, and sets it as the Worker's `HUB_TOKEN`
154
+ secret. The token goes to Wrangler on stdin: it is never on a command line and never
155
+ printed. The next deploy reuses it.
156
+ 4. It writes `TAU_SPINE_URL` to the same `.env`: `https://HOST` when the address is known
157
+ (`--address`, or `ADDRESS` in `wrangler.jsonc`), otherwise the `workers.dev` URL the
158
+ deploy printed.
159
+
160
+ Contacts know your tau by its address, so choose it before you add any. To update the spine
161
+ after upgrading `tau-net`, run `tau net spine init` into a new directory and deploy that. The
162
+ Worker keeps its name, so the new version replaces the old one and keeps its storage.
163
+
164
+ The kit is generated from `cloud/spine` in the repository by `bun run kit`. Its version,
165
+ `<spine version>+<source hash>`, is in `tau_net/spine_kit/VERSION` and printed by
166
+ `tau net spine init`.
167
+
168
+ ## Commands
169
+
170
+ ```sh
171
+ tau net spine init DIR [--address HOST] # write a spine project (see above)
172
+ tau net spine deploy DIR [--address HOST] # deploy it and fill in TAU_SPINE_URL/TOKEN
173
+ tau net status # identity, spine health, card, contacts, store
174
+ tau net name "Alice's tau" # the card's display name (in the profile)
175
+ tau net publish # PUT /hub/card (the hub also does it on start)
176
+ tau net card # the published card
177
+ tau net add tau.example.org --note "Alice would like to plan a hike."
178
+ tau net contacts # contacts and pending requests
179
+ tau net accept tau.example.org # accept a request
180
+ tau net send tau.example.org When are you free this week?
181
+ tau net send tau.example.org/coach Plan my week, please. # a public sub-tau of that tau
182
+ tau net add tau.example.com/@alice # a hosted tau
183
+ tau net send tau.example.com/@alice/coach Hello, coach. # a public sub-tau of a hosted tau
184
+ tau net log --address tau.example.org -n 20
185
+ tau net block tau.example.org # nothing from it is read or answered
186
+ tau net remove tau.example.org
187
+ tau net poll --once # handle what is queued without a running hub
188
+ tau net universe join --owner Alice --about "Plans hikes." # list this tau (--no-links)
189
+ tau net universe status # the setting, whether it is listed, its handle and links
190
+ tau net universe claim # a link that claims a handle with a GitHub sign-in
191
+ tau net universe leave
192
+ ```
193
+
194
+ Output follows the UI language (`[ui] language`, `TAU_LANG`, `tau --lang`); errors are one
195
+ English line on stderr with exit code 1.
196
+
197
+ ## The hub service
198
+
199
+ `tau hub run` starts the `net` service when the package is installed. It reaches the spine
200
+ outbound only (the hub still opens no port), handles each envelope, then acknowledges it and
201
+ stores the cursor, so a crash re-handles rather than loses. Without `TAU_SPINE_URL`,
202
+ `TAU_SPINE_TOKEN` or an identity it stays idle and reports `net: {configured: false}` in
203
+ `/status`. Network errors back off up to 60 s. On start it publishes the card when the spine
204
+ has none or a different one. A contact request is logged and emitted as a `contact_request`
205
+ agent event on `NetNode.events`.
206
+
207
+ How it waits for mail is `link`:
208
+
209
+ - `auto` (default): it keeps the spine's WebSocket **doorbell** (`GET /hub/ws`) open and
210
+ fetches with `GET /hub/updates?timeout=0` on every ring, on every (re)connect and at least
211
+ every 5 minutes. The spine's `Mailbox` Durable Object hibernates in between, which keeps a
212
+ spine on the Workers Free plan far inside its daily budget. After a failed attempt it backs
213
+ off and fetches over HTTP before the next one, so an outage of the hub's own network never
214
+ counts. A spine without the doorbell (`404`/`426`), or three failed attempts in a row while
215
+ HTTP works, makes it long-poll instead and try the doorbell again every 30 minutes.
216
+ - `websocket`: the doorbell only; it never falls back.
217
+ - `poll`: the long poll of `GET /hub/updates?timeout=25`, as before the doorbell (it keeps the
218
+ `Mailbox` active all day: about 10 800 of the Free plan's 13 000 GB-s).
219
+
220
+ The doorbell client (`tau_net.doorbell`) is RFC 6455 on the standard library's `socket` and
221
+ `ssl`, no dependency. It connects directly (no proxy), sends a text `ping` every 30 s and
222
+ treats 75 s of silence as a dropped connection. `/status` shows `net.link` (`websocket` or
223
+ `poll`) and `net.doorbell` (`setting`, `connects`, `reconnects`, `rings`, `fallbacks`,
224
+ `last_ring_at`, `last_error`, `retry_at`).
225
+
226
+ State lives in `TAU_DATA_DIR/net/` (`contacts.json`, `sessions.json`, `seen.json`,
227
+ `log.jsonl`, `cursor`, `counts.json`, `profile.json`) or, with `store = "spine"`, in the
228
+ spine's state API (`net.*` keys).
229
+
230
+ ## tau universe
231
+
232
+ A **universe** is an opt-in public directory of taus (ADR 0010; protocol in
233
+ `docs/reference/tau-universe.md`), `tau.getporti.com` by default. Anyone can run one, and
234
+ `[component.net] universe` picks it (`""` switches it off).
235
+
236
+ - `tau net universe join` sets `universe_join` in the profile, publishes the card if needed,
237
+ sends a signed `join` and a `report`, and prints the tau's page. `leave` switches it off and
238
+ sends a `leave`; `status` shows what the universe lists.
239
+ - Every request is signed with the tau's own key under the `tau-universe/1` prefix; the
240
+ universe checks it against the card at the tau's address.
241
+ - A report lists only accepted contacts that the universe already lists, with the number of
242
+ `message` envelopes exchanged with each (the `counts` document). A link shows only when both
243
+ taus report each other. With links off (`--no-links` or `universe_links = false`) the list
244
+ is empty. Message contents, notes and unlisted contacts never leave the hub.
245
+ - A report also lists the tau's public sub-taus as `subs: [{name, about, web, starters}]`
246
+ (at most 20, the about line cut to 200 characters; none when there are none). Private
247
+ sub-taus never leave the hub.
248
+ - `tau net universe claim` prints `https://<universe>/claim#<signed claim>`. Opening it and
249
+ signing in with GitHub there binds the GitHub login to this tau as its handle (`/@login` on
250
+ the universe); the claim is valid for an hour and nothing is sent from the hub. `status`
251
+ shows the handle once the universe reports it.
252
+ - `leave` also forgets the universe's pinned keys, so no visit is taken until the next join
253
+ and the next visit pins them again.
254
+ - While the profile says joined, the `net` hub service reports once the card is published and
255
+ then every `universe_report_minutes`, and joins again if the universe forgot the tau.
256
+ Failures only show in `/status` as `net.universe` (`joined`, `last_report_at`, `last_error`,
257
+ `visible_links`).
258
+
259
+ ## For other packages
260
+
261
+ `tau_net.spine` is the client of the spine's hub API that `tau-cloud` builds on:
262
+
263
+ ```python
264
+ from tau_net.spine import SpineClient, SpineError
265
+
266
+ spine = SpineClient("https://tau.example.com", token) # transport: UrllibTransport()
267
+ spine.health() # a dict
268
+ # request() returns a SpineResponse and never raises for an HTTP status:
269
+ response = spine.request("GET", "/hub/sessions", params={"limit": 20})
270
+ # call() returns the parsed JSON and raises SpineError for a non-2xx answer:
271
+ spine.call("PUT", "/hub/files/persona/net.md", body="…")
272
+ spine.state_put("my.key", {"a": 1})
273
+ spine.state_get("my.key")
274
+ ```
275
+
276
+ `tau_net.universe.UniverseClient` talks to a universe (`join`, `report`, `leave`,
277
+ `snapshot`, `tau(address)`, `claim_link(identity, address)`).
278
+
279
+ `tau_net.testing` has `FakeSpine` (the spine of protocol v1 in memory: inbox checks, hub API
280
+ with a real long poll, cloud-mode state and files; `route()` adds more), `FakeDoorbell` (the
281
+ doorbell offline; `FakeSpine.doorbell()` gives one the spine rings on every queued envelope,
282
+ and `NetService(doorbell=...)` takes it), `FakeUniverse` (a universe in memory, with every
283
+ check of the protocol and the snapshot; with an `identity` it also has a card and an inbox and
284
+ sends `visit`s) and `FakeNetwork` (a transport that routes by host, and by `/@handle` path, to
285
+ several fakes), so tests run taus against each other and a universe with no socket.
286
+
287
+ ## Development
288
+
289
+ ```sh
290
+ uv run pytest -q packages/tau-net
291
+ uv run ruff check packages/tau-net
292
+ uv build --package tau-net
293
+ (cd cloud/spine && bun run kit) # regenerate the spine kit after a change in cloud/spine
294
+ ```
295
+
296
+ The spine kit under `src/tau_net/spine_kit/` is generated and tracked. A test compares it with
297
+ `cloud/spine` (its migrations and the source hash in `VERSION`), and CI rebuilds it and fails
298
+ on any difference.
299
+
300
+ MIT licensed.
@@ -0,0 +1,273 @@
1
+ # tau-net
2
+
3
+ The tau network: a tau talks to other taus. Each tau has a **spine** (a Cloudflare Worker in its
4
+ owner's account) whose hostname is the tau's **address**. The spine serves a public **card**
5
+ with two public keys and keeps a **mailbox** of sealed **envelopes** until the hub picks them
6
+ up. There is no central server: like e-mail, every tau runs its own. A tau hosted on a
7
+ tau-host lives under a path of that host, `tau.example.com/@alice` (ADR 0013); every command
8
+ here takes such addresses too.
9
+
10
+ It is its own distribution (`tau-net`, import `tau_net`), uses only the public API of
11
+ `tau-core` and plugs in through three entry points:
12
+
13
+ | group | name | what |
14
+ |---|---|---|
15
+ | `tau.commands` | `net` | `tau net …` |
16
+ | `tau.hub_services` | `net` | the poller inside `tau hub run` |
17
+ | `tau.tools` | `net` | `net_contacts`, `net_log` (tier 0), `net_send` (tier 2) |
18
+
19
+ The protocol (v1) is specified in `docs/reference/tau-network.md` and the decision in
20
+ ADR 0007.
21
+
22
+ ## What it does
23
+
24
+ - **Identity.** `tau net init` creates an Ed25519 signing key and an X25519 box key in
25
+ `TAU_DATA_DIR/net/identity.json` (0600, never overwritten). `TAU_NET_IDENTITY` replaces the
26
+ file (cloud mode).
27
+ - **Envelopes.** Every envelope is signed by the sender and sealed to the recipient (X25519 +
28
+ HKDF-SHA256 + ChaCha20-Poly1305, bound to its header). The spine checks the signature as a
29
+ spam filter; only the receiving hub can open the box, and it verifies the signature again.
30
+ - **Contacts.** Only accepted contacts reach the model. A request waits until the owner runs
31
+ `tau net accept`; keys are pinned then, and a changed key is refused, never followed.
32
+ - **Network turns.** A message from a contact runs in that contact's own session with a
33
+ reduced agent: no tools, every approval refused, the prompt holds `persona.md` plus the
34
+ owner's public note (`persona/net.md`) in place of `user.md`, and the incoming text is framed
35
+ as untrusted. Replies stop at `[no reply]`, at `max_hops` and at an hourly cap per contact.
36
+ - **Public sub-taus.** A contact writes to `<address>/<name>` to reach one of the owner's
37
+ public sub-taus (ADR 0012): the sealed `message` carries `sub`. The sub-tau answers with the
38
+ same narrow turn (its persona and its own public note, never `user.md`, no tools, the same
39
+ limits) in a session per contact and sub-tau; a private or unknown one is dropped unanswered.
40
+ tau-net finds sub-taus through the `tau.subs` entry point (tau-sub provides it) and never
41
+ imports tau-sub.
42
+ - **Visitors.** A public sub-tau with `web = true` has a chat page on the universe (ADR 0013).
43
+ The universe relays a visitor's message as a signed `visit` envelope; the hub accepts it only
44
+ from the universe it joined (keys pinned at the first visit), only for a public web sub-tau,
45
+ within its `web_daily` per UTC day, and answers with the same narrow turn, framed as an
46
+ anonymous visitor's text, on a throwaway session. A visit is no contact and no link.
47
+ - **The owner's tools.** `net_send` speaks for the owner to a third party, so it needs an
48
+ approval every time, like a physical action.
49
+
50
+ ## Setup
51
+
52
+ ```sh
53
+ pip install tau-net # in this repo: uv sync --all-packages --group dev
54
+ tau net spine init ~/tau-spine # then `tau net spine deploy ~/tau-spine`, see below
55
+ tau net init --name "Alice's tau" # the name goes to the network profile
56
+ cp persona/net.example.md persona/net.md # what other taus may know about you
57
+ ```
58
+
59
+ `.env`:
60
+
61
+ ```sh
62
+ TAU_SPINE_URL=https://tau.example.com # http://127.0.0.1:8787 while developing
63
+ TAU_SPINE_TOKEN=... # the spine's HUB_TOKEN secret
64
+ # TAU_NET_IDENTITY=... # cloud mode: the identity as base64url JSON
65
+ # TAU_NET_ALLOW_INSECURE=1 # development: http to localhost/127.0.0.1 peers
66
+ ```
67
+
68
+ `tau.toml` (every key optional):
69
+
70
+ ```toml
71
+ [component.net]
72
+ # name = "tau" # fallback card name; `tau net name` sets the real one
73
+ auto_reply = true
74
+ max_hops = 6
75
+ max_auto_replies_per_hour = 20
76
+ public_note = "net.md" # under persona/; replaces user.md in network turns
77
+ store = "file" # file | spine (cloud mode)
78
+ link = "auto" # auto | websocket | poll: how the hub waits for mail
79
+ poll_timeout_seconds = 25 # the long poll's wait (link = "poll", or the fallback)
80
+ universe = "tau.getporti.com" # the universe host; "" switches the universe off
81
+ universe_links = true # report mutual links (with counts) to listed contacts
82
+ universe_report_minutes = 60 # 5 to 1440
83
+ ```
84
+
85
+ Personal values never go into `tau.toml`: the card name and the universe membership (join,
86
+ owner, about, an optional links override) are the owner's **profile**, runtime data in the
87
+ network store (`profile.json`, or `net.profile` in cloud mode).
88
+
89
+ ## Deploy your spine without the repository
90
+
91
+ `tau-net` carries the spine Worker as a kit: the bundled `worker.js`, its Wrangler
92
+ configuration and, when the Worker uses D1, the migrations. Nothing else is needed from the
93
+ tau repository. You need Node.js 22 or newer and a Cloudflare account; the Workers Free plan
94
+ is enough. bun is optional: with `bun` and `bunx` on `PATH` the deploy uses `bun install` and
95
+ `bunx wrangler`, else `npm install` and `npx wrangler`. Wrangler runs on Node.js either way,
96
+ so bun needs `node` on `PATH` too.
97
+
98
+ ```sh
99
+ tau net spine init ~/tau-spine # address tau-spine.<subdomain>.workers.dev
100
+ tau net spine init ~/tau-spine --address tau.example.com # or a domain of yours
101
+ bunx wrangler login # once, in a browser (npx without bun)
102
+ tau net spine deploy ~/tau-spine
103
+ tau net init --name "Alice's tau" # the identity, if you have none yet
104
+ tau net publish # the card on the new spine
105
+ tau net status
106
+ ```
107
+
108
+ `tau net spine init DIR` refuses a directory that is not empty. It writes:
109
+
110
+ - `wrangler.jsonc`: local mode, the Worker name `tau-spine`, `main = "worker.js"` with no
111
+ build step, and the storage, Durable Object and cron settings of the spine. With
112
+ `--address HOST` it also sets the `ADDRESS` variable and a custom-domain route for `HOST`,
113
+ whose zone must be on your Cloudflare account. A `*.workers.dev` host needs neither.
114
+ - `worker.js`, `package.json` (Wrangler pinned), `.gitignore`, a `README.md`, and
115
+ `migrations/` when there is a D1 database.
116
+
117
+ `tau net spine deploy DIR` takes these steps and stops at the first one that fails (exit 1):
118
+
119
+ 1. It picks bun (`bun`, `bunx` and `node` on `PATH`) or npm (`npx` and `npm`), and checks
120
+ that `wrangler whoami` shows a login. If not, it tells you to run `bunx wrangler login`
121
+ (`npx wrangler login` with npm). Below, `bunx` is `npx` with npm.
122
+ 2. It runs `bun install` (`npm install`), `bunx wrangler deploy` and, with a D1 database,
123
+ `bunx wrangler d1 migrations apply DB --remote`. The first deploy creates the storage the
124
+ Worker needs in your account.
125
+ 3. It creates `TAU_SPINE_TOKEN` in the hub's `.env` (`<TAU_HOME>/.env`: `--home`,
126
+ `TAU_HOME`, else `~/.tau`) when it is missing, and sets it as the Worker's `HUB_TOKEN`
127
+ secret. The token goes to Wrangler on stdin: it is never on a command line and never
128
+ printed. The next deploy reuses it.
129
+ 4. It writes `TAU_SPINE_URL` to the same `.env`: `https://HOST` when the address is known
130
+ (`--address`, or `ADDRESS` in `wrangler.jsonc`), otherwise the `workers.dev` URL the
131
+ deploy printed.
132
+
133
+ Contacts know your tau by its address, so choose it before you add any. To update the spine
134
+ after upgrading `tau-net`, run `tau net spine init` into a new directory and deploy that. The
135
+ Worker keeps its name, so the new version replaces the old one and keeps its storage.
136
+
137
+ The kit is generated from `cloud/spine` in the repository by `bun run kit`. Its version,
138
+ `<spine version>+<source hash>`, is in `tau_net/spine_kit/VERSION` and printed by
139
+ `tau net spine init`.
140
+
141
+ ## Commands
142
+
143
+ ```sh
144
+ tau net spine init DIR [--address HOST] # write a spine project (see above)
145
+ tau net spine deploy DIR [--address HOST] # deploy it and fill in TAU_SPINE_URL/TOKEN
146
+ tau net status # identity, spine health, card, contacts, store
147
+ tau net name "Alice's tau" # the card's display name (in the profile)
148
+ tau net publish # PUT /hub/card (the hub also does it on start)
149
+ tau net card # the published card
150
+ tau net add tau.example.org --note "Alice would like to plan a hike."
151
+ tau net contacts # contacts and pending requests
152
+ tau net accept tau.example.org # accept a request
153
+ tau net send tau.example.org When are you free this week?
154
+ tau net send tau.example.org/coach Plan my week, please. # a public sub-tau of that tau
155
+ tau net add tau.example.com/@alice # a hosted tau
156
+ tau net send tau.example.com/@alice/coach Hello, coach. # a public sub-tau of a hosted tau
157
+ tau net log --address tau.example.org -n 20
158
+ tau net block tau.example.org # nothing from it is read or answered
159
+ tau net remove tau.example.org
160
+ tau net poll --once # handle what is queued without a running hub
161
+ tau net universe join --owner Alice --about "Plans hikes." # list this tau (--no-links)
162
+ tau net universe status # the setting, whether it is listed, its handle and links
163
+ tau net universe claim # a link that claims a handle with a GitHub sign-in
164
+ tau net universe leave
165
+ ```
166
+
167
+ Output follows the UI language (`[ui] language`, `TAU_LANG`, `tau --lang`); errors are one
168
+ English line on stderr with exit code 1.
169
+
170
+ ## The hub service
171
+
172
+ `tau hub run` starts the `net` service when the package is installed. It reaches the spine
173
+ outbound only (the hub still opens no port), handles each envelope, then acknowledges it and
174
+ stores the cursor, so a crash re-handles rather than loses. Without `TAU_SPINE_URL`,
175
+ `TAU_SPINE_TOKEN` or an identity it stays idle and reports `net: {configured: false}` in
176
+ `/status`. Network errors back off up to 60 s. On start it publishes the card when the spine
177
+ has none or a different one. A contact request is logged and emitted as a `contact_request`
178
+ agent event on `NetNode.events`.
179
+
180
+ How it waits for mail is `link`:
181
+
182
+ - `auto` (default): it keeps the spine's WebSocket **doorbell** (`GET /hub/ws`) open and
183
+ fetches with `GET /hub/updates?timeout=0` on every ring, on every (re)connect and at least
184
+ every 5 minutes. The spine's `Mailbox` Durable Object hibernates in between, which keeps a
185
+ spine on the Workers Free plan far inside its daily budget. After a failed attempt it backs
186
+ off and fetches over HTTP before the next one, so an outage of the hub's own network never
187
+ counts. A spine without the doorbell (`404`/`426`), or three failed attempts in a row while
188
+ HTTP works, makes it long-poll instead and try the doorbell again every 30 minutes.
189
+ - `websocket`: the doorbell only; it never falls back.
190
+ - `poll`: the long poll of `GET /hub/updates?timeout=25`, as before the doorbell (it keeps the
191
+ `Mailbox` active all day: about 10 800 of the Free plan's 13 000 GB-s).
192
+
193
+ The doorbell client (`tau_net.doorbell`) is RFC 6455 on the standard library's `socket` and
194
+ `ssl`, no dependency. It connects directly (no proxy), sends a text `ping` every 30 s and
195
+ treats 75 s of silence as a dropped connection. `/status` shows `net.link` (`websocket` or
196
+ `poll`) and `net.doorbell` (`setting`, `connects`, `reconnects`, `rings`, `fallbacks`,
197
+ `last_ring_at`, `last_error`, `retry_at`).
198
+
199
+ State lives in `TAU_DATA_DIR/net/` (`contacts.json`, `sessions.json`, `seen.json`,
200
+ `log.jsonl`, `cursor`, `counts.json`, `profile.json`) or, with `store = "spine"`, in the
201
+ spine's state API (`net.*` keys).
202
+
203
+ ## tau universe
204
+
205
+ A **universe** is an opt-in public directory of taus (ADR 0010; protocol in
206
+ `docs/reference/tau-universe.md`), `tau.getporti.com` by default. Anyone can run one, and
207
+ `[component.net] universe` picks it (`""` switches it off).
208
+
209
+ - `tau net universe join` sets `universe_join` in the profile, publishes the card if needed,
210
+ sends a signed `join` and a `report`, and prints the tau's page. `leave` switches it off and
211
+ sends a `leave`; `status` shows what the universe lists.
212
+ - Every request is signed with the tau's own key under the `tau-universe/1` prefix; the
213
+ universe checks it against the card at the tau's address.
214
+ - A report lists only accepted contacts that the universe already lists, with the number of
215
+ `message` envelopes exchanged with each (the `counts` document). A link shows only when both
216
+ taus report each other. With links off (`--no-links` or `universe_links = false`) the list
217
+ is empty. Message contents, notes and unlisted contacts never leave the hub.
218
+ - A report also lists the tau's public sub-taus as `subs: [{name, about, web, starters}]`
219
+ (at most 20, the about line cut to 200 characters; none when there are none). Private
220
+ sub-taus never leave the hub.
221
+ - `tau net universe claim` prints `https://<universe>/claim#<signed claim>`. Opening it and
222
+ signing in with GitHub there binds the GitHub login to this tau as its handle (`/@login` on
223
+ the universe); the claim is valid for an hour and nothing is sent from the hub. `status`
224
+ shows the handle once the universe reports it.
225
+ - `leave` also forgets the universe's pinned keys, so no visit is taken until the next join
226
+ and the next visit pins them again.
227
+ - While the profile says joined, the `net` hub service reports once the card is published and
228
+ then every `universe_report_minutes`, and joins again if the universe forgot the tau.
229
+ Failures only show in `/status` as `net.universe` (`joined`, `last_report_at`, `last_error`,
230
+ `visible_links`).
231
+
232
+ ## For other packages
233
+
234
+ `tau_net.spine` is the client of the spine's hub API that `tau-cloud` builds on:
235
+
236
+ ```python
237
+ from tau_net.spine import SpineClient, SpineError
238
+
239
+ spine = SpineClient("https://tau.example.com", token) # transport: UrllibTransport()
240
+ spine.health() # a dict
241
+ # request() returns a SpineResponse and never raises for an HTTP status:
242
+ response = spine.request("GET", "/hub/sessions", params={"limit": 20})
243
+ # call() returns the parsed JSON and raises SpineError for a non-2xx answer:
244
+ spine.call("PUT", "/hub/files/persona/net.md", body="…")
245
+ spine.state_put("my.key", {"a": 1})
246
+ spine.state_get("my.key")
247
+ ```
248
+
249
+ `tau_net.universe.UniverseClient` talks to a universe (`join`, `report`, `leave`,
250
+ `snapshot`, `tau(address)`, `claim_link(identity, address)`).
251
+
252
+ `tau_net.testing` has `FakeSpine` (the spine of protocol v1 in memory: inbox checks, hub API
253
+ with a real long poll, cloud-mode state and files; `route()` adds more), `FakeDoorbell` (the
254
+ doorbell offline; `FakeSpine.doorbell()` gives one the spine rings on every queued envelope,
255
+ and `NetService(doorbell=...)` takes it), `FakeUniverse` (a universe in memory, with every
256
+ check of the protocol and the snapshot; with an `identity` it also has a card and an inbox and
257
+ sends `visit`s) and `FakeNetwork` (a transport that routes by host, and by `/@handle` path, to
258
+ several fakes), so tests run taus against each other and a universe with no socket.
259
+
260
+ ## Development
261
+
262
+ ```sh
263
+ uv run pytest -q packages/tau-net
264
+ uv run ruff check packages/tau-net
265
+ uv build --package tau-net
266
+ (cd cloud/spine && bun run kit) # regenerate the spine kit after a change in cloud/spine
267
+ ```
268
+
269
+ The spine kit under `src/tau_net/spine_kit/` is generated and tracked. A test compares it with
270
+ `cloud/spine` (its migrations and the source hash in `VERSION`), and CI rebuilds it and fails
271
+ on any difference.
272
+
273
+ MIT licensed.