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.
- tau_net-0.1.0/.gitignore +29 -0
- tau_net-0.1.0/LICENSE +21 -0
- tau_net-0.1.0/PKG-INFO +300 -0
- tau_net-0.1.0/README.md +273 -0
- tau_net-0.1.0/pyproject.toml +50 -0
- tau_net-0.1.0/scripts/make_ts_vectors.py +741 -0
- tau_net-0.1.0/src/tau_net/__init__.py +195 -0
- tau_net-0.1.0/src/tau_net/cli.py +803 -0
- tau_net-0.1.0/src/tau_net/config.py +227 -0
- tau_net-0.1.0/src/tau_net/contacts.py +349 -0
- tau_net-0.1.0/src/tau_net/doorbell.py +519 -0
- tau_net-0.1.0/src/tau_net/encoding.py +236 -0
- tau_net-0.1.0/src/tau_net/envelope.py +465 -0
- tau_net-0.1.0/src/tau_net/identity.py +203 -0
- tau_net-0.1.0/src/tau_net/node.py +1102 -0
- tau_net-0.1.0/src/tau_net/peers.py +204 -0
- tau_net-0.1.0/src/tau_net/profile.py +107 -0
- tau_net-0.1.0/src/tau_net/service.py +645 -0
- tau_net-0.1.0/src/tau_net/spine.py +419 -0
- tau_net-0.1.0/src/tau_net/spine_kit/README.md +58 -0
- tau_net-0.1.0/src/tau_net/spine_kit/VERSION +1 -0
- tau_net-0.1.0/src/tau_net/spine_kit/package.template.json +16 -0
- tau_net-0.1.0/src/tau_net/spine_kit/worker.js +2770 -0
- tau_net-0.1.0/src/tau_net/spine_kit/wrangler.template.jsonc +78 -0
- tau_net-0.1.0/src/tau_net/spine_kit.py +572 -0
- tau_net-0.1.0/src/tau_net/spine_strings.py +95 -0
- tau_net-0.1.0/src/tau_net/store.py +395 -0
- tau_net-0.1.0/src/tau_net/strings.py +252 -0
- tau_net-0.1.0/src/tau_net/subs.py +180 -0
- tau_net-0.1.0/src/tau_net/testing.py +1219 -0
- tau_net-0.1.0/src/tau_net/tools.py +187 -0
- tau_net-0.1.0/src/tau_net/turns.py +438 -0
- tau_net-0.1.0/src/tau_net/universe.py +659 -0
- tau_net-0.1.0/tests/conftest.py +59 -0
- tau_net-0.1.0/tests/net_helpers.py +150 -0
- tau_net-0.1.0/tests/test_claim.py +157 -0
- tau_net-0.1.0/tests/test_cli.py +195 -0
- tau_net-0.1.0/tests/test_doorbell.py +436 -0
- tau_net-0.1.0/tests/test_guard_opt_out.py +23 -0
- tau_net-0.1.0/tests/test_handles.py +279 -0
- tau_net-0.1.0/tests/test_link.py +386 -0
- tau_net-0.1.0/tests/test_node.py +396 -0
- tau_net-0.1.0/tests/test_package.py +54 -0
- tau_net-0.1.0/tests/test_profile.py +111 -0
- tau_net-0.1.0/tests/test_protocol.py +350 -0
- tau_net-0.1.0/tests/test_service.py +283 -0
- tau_net-0.1.0/tests/test_spine.py +353 -0
- tau_net-0.1.0/tests/test_spine_kit.py +637 -0
- tau_net-0.1.0/tests/test_state.py +217 -0
- tau_net-0.1.0/tests/test_subs.py +542 -0
- tau_net-0.1.0/tests/test_tools.py +120 -0
- tau_net-0.1.0/tests/test_transport.py +82 -0
- tau_net-0.1.0/tests/test_ts_vectors.py +161 -0
- tau_net-0.1.0/tests/test_universe.py +260 -0
- tau_net-0.1.0/tests/test_universe_hub.py +450 -0
- tau_net-0.1.0/tests/test_visits.py +428 -0
tau_net-0.1.0/.gitignore
ADDED
|
@@ -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.
|
tau_net-0.1.0/README.md
ADDED
|
@@ -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.
|