@nervur-org/dock 0.3.0
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.
- package/GETTING_STARTED.md +128 -0
- package/LICENSE +202 -0
- package/NOTICE +6 -0
- package/README.md +37 -0
- package/api/quo-api.md +196 -0
- package/api/route.ts +227 -0
- package/beings/GLOSSARY.md +118 -0
- package/beings/WORLDS.md +567 -0
- package/beings/avatar.ts +101 -0
- package/beings/carry.ts +159 -0
- package/beings/clock.ts +552 -0
- package/beings/courier.ts +107 -0
- package/beings/desk.ts +128 -0
- package/beings/doorbell.ts +129 -0
- package/beings/envoy.ts +218 -0
- package/beings/index.ts +16 -0
- package/beings/join.ts +51 -0
- package/beings/link.ts +104 -0
- package/beings/look.ts +139 -0
- package/beings/porter.ts +43 -0
- package/beings/presence.ts +39 -0
- package/beings/quo-app-bells.md +280 -0
- package/beings/quo-dock.md +1942 -0
- package/beings/setup.ts +45 -0
- package/beings/side.ts +60 -0
- package/beings/user.ts +459 -0
- package/beings/way.ts +65 -0
- package/cli/bundles.ts +20 -0
- package/cli/client.ts +73 -0
- package/cli/daemon.ts +581 -0
- package/cli/estate/Caddyfile +33 -0
- package/cli/estate/quo.service +35 -0
- package/cli/estate/systems.quo.plist +44 -0
- package/cli/estate.ts +48 -0
- package/cli/folder.ts +27 -0
- package/cli/oauth.ts +32 -0
- package/cli/pilot.ts +38 -0
- package/cli/quo-dock.md +1942 -0
- package/cli/quo.ts +203 -0
- package/cli/stand.ts +160 -0
- package/dist/api/route.d.ts +21 -0
- package/dist/api/route.js +193 -0
- package/dist/beings/avatar.d.ts +32 -0
- package/dist/beings/avatar.js +100 -0
- package/dist/beings/carry.d.ts +12 -0
- package/dist/beings/carry.js +174 -0
- package/dist/beings/clock.d.ts +161 -0
- package/dist/beings/clock.js +528 -0
- package/dist/beings/courier.d.ts +50 -0
- package/dist/beings/courier.js +96 -0
- package/dist/beings/desk.d.ts +53 -0
- package/dist/beings/desk.js +135 -0
- package/dist/beings/doorbell.d.ts +78 -0
- package/dist/beings/doorbell.js +133 -0
- package/dist/beings/envoy.d.ts +54 -0
- package/dist/beings/envoy.js +211 -0
- package/dist/beings/index.d.ts +14 -0
- package/dist/beings/index.js +16 -0
- package/dist/beings/join.d.ts +15 -0
- package/dist/beings/join.js +42 -0
- package/dist/beings/link.d.ts +14 -0
- package/dist/beings/link.js +91 -0
- package/dist/beings/look.d.ts +30 -0
- package/dist/beings/look.js +104 -0
- package/dist/beings/porter.d.ts +14 -0
- package/dist/beings/porter.js +41 -0
- package/dist/beings/presence.d.ts +8 -0
- package/dist/beings/presence.js +21 -0
- package/dist/beings/setup.d.ts +3 -0
- package/dist/beings/setup.js +39 -0
- package/dist/beings/side.d.ts +32 -0
- package/dist/beings/side.js +37 -0
- package/dist/beings/user.d.ts +315 -0
- package/dist/beings/user.js +462 -0
- package/dist/beings/way.d.ts +19 -0
- package/dist/beings/way.js +28 -0
- package/dist/cli/bundles.d.ts +5 -0
- package/dist/cli/bundles.js +17 -0
- package/dist/cli/client.d.ts +15 -0
- package/dist/cli/client.js +58 -0
- package/dist/cli/daemon.d.ts +50 -0
- package/dist/cli/daemon.js +584 -0
- package/dist/cli/estate/Caddyfile +33 -0
- package/dist/cli/estate/quo.service +35 -0
- package/dist/cli/estate/systems.quo.plist +44 -0
- package/dist/cli/estate.d.ts +5 -0
- package/dist/cli/estate.js +50 -0
- package/dist/cli/folder.d.ts +2 -0
- package/dist/cli/folder.js +25 -0
- package/dist/cli/oauth.d.ts +2 -0
- package/dist/cli/oauth.js +32 -0
- package/dist/cli/pilot.d.ts +5 -0
- package/dist/cli/pilot.js +22 -0
- package/dist/cli/quo.d.ts +2 -0
- package/dist/cli/quo.js +201 -0
- package/dist/cli/stand.d.ts +37 -0
- package/dist/cli/stand.js +110 -0
- package/dist/dock/agents.d.ts +102 -0
- package/dist/dock/agents.js +105 -0
- package/dist/dock/apns.d.ts +27 -0
- package/dist/dock/apns.js +98 -0
- package/dist/dock/console.d.ts +6 -0
- package/dist/dock/console.js +20 -0
- package/dist/dock/dialers.d.ts +56 -0
- package/dist/dock/dialers.js +75 -0
- package/dist/dock/faculty.d.ts +24 -0
- package/dist/dock/faculty.js +142 -0
- package/dist/dock/fcm.d.ts +13 -0
- package/dist/dock/fcm.js +77 -0
- package/dist/dock/grants.d.ts +41 -0
- package/dist/dock/grants.js +99 -0
- package/dist/dock/http2.d.ts +4 -0
- package/dist/dock/http2.js +68 -0
- package/dist/dock/index.d.ts +41 -0
- package/dist/dock/index.js +113 -0
- package/dist/dock/local.d.ts +13 -0
- package/dist/dock/local.js +44 -0
- package/dist/dock/location.d.ts +19 -0
- package/dist/dock/location.js +11 -0
- package/dist/dock/notifications.d.ts +38 -0
- package/dist/dock/notifications.js +60 -0
- package/dist/dock/push.d.ts +79 -0
- package/dist/dock/push.js +132 -0
- package/dist/dock/service.d.ts +4 -0
- package/dist/dock/service.js +107 -0
- package/dist/dock/timer.d.ts +52 -0
- package/dist/dock/timer.js +149 -0
- package/dist/dock/webpush.d.ts +12 -0
- package/dist/dock/webpush.js +144 -0
- package/dist/harbor/browser.d.ts +15 -0
- package/dist/harbor/browser.js +82 -0
- package/dist/harbor/capacitor.d.ts +28 -0
- package/dist/harbor/capacitor.js +218 -0
- package/dist/harbor/disk.d.ts +27 -0
- package/dist/harbor/disk.js +249 -0
- package/dist/harbor/edge/alarm.d.ts +10 -0
- package/dist/harbor/edge/alarm.js +38 -0
- package/dist/harbor/edge/edge.d.ts +54 -0
- package/dist/harbor/edge/edge.js +370 -0
- package/dist/harbor/edge/exercise.d.ts +7 -0
- package/dist/harbor/edge/exercise.js +232 -0
- package/dist/harbor/edge/given.d.ts +26 -0
- package/dist/harbor/edge/given.js +42 -0
- package/dist/harbor/edge/platform.d.ts +39 -0
- package/dist/harbor/edge/storage.d.ts +30 -0
- package/dist/harbor/edge/storage.js +180 -0
- package/dist/harbor/edge/worker.d.ts +17 -0
- package/dist/harbor/edge/worker.js +67 -0
- package/dist/harbor/files.d.ts +15 -0
- package/dist/harbor/files.js +220 -0
- package/dist/harbor/http.d.ts +19 -0
- package/dist/harbor/http.js +177 -0
- package/dist/harbor/idb.d.ts +15 -0
- package/dist/harbor/idb.js +0 -0
- package/dist/harbor/quo.d.ts +10 -0
- package/dist/harbor/quo.js +42 -0
- package/dist/harbor/resolve.d.ts +16 -0
- package/dist/harbor/resolve.js +17 -0
- package/dist/harbor/seal.d.ts +3 -0
- package/dist/harbor/seal.js +25 -0
- package/dist/harbor/sealed.d.ts +35 -0
- package/dist/harbor/sealed.js +162 -0
- package/dist/harbor/tauri.d.ts +16 -0
- package/dist/harbor/tauri.js +59 -0
- package/dist/harbor/ward.d.ts +9 -0
- package/dist/harbor/ward.js +15 -0
- package/dist/human/app.d.ts +32 -0
- package/dist/human/app.js +527 -0
- package/dist/human/apps.d.ts +10 -0
- package/dist/human/apps.js +29 -0
- package/dist/human/dom.d.ts +2 -0
- package/dist/human/dom.js +15 -0
- package/dist/human/door.d.ts +19 -0
- package/dist/human/door.js +92 -0
- package/dist/human/guest.d.ts +3 -0
- package/dist/human/guest.js +33 -0
- package/dist/human/html.d.ts +55 -0
- package/dist/human/html.js +451 -0
- package/dist/human/local.d.ts +7 -0
- package/dist/human/local.js +10 -0
- package/dist/human/mark.d.ts +8 -0
- package/dist/human/mark.js +47 -0
- package/dist/human/move.d.ts +17 -0
- package/dist/human/move.js +90 -0
- package/dist/human/screen.d.ts +19 -0
- package/dist/human/screen.js +119 -0
- package/dist/human/style.d.ts +2 -0
- package/dist/human/style.js +60 -0
- package/dist/human/tab.bundle.txt +4775 -0
- package/dist/human/tab.d.ts +15 -0
- package/dist/human/tab.js +409 -0
- package/dist/human/tree.d.ts +50 -0
- package/dist/human/tree.js +157 -0
- package/dist/human/wake.d.ts +34 -0
- package/dist/human/wake.js +179 -0
- package/dist/human/web.d.ts +33 -0
- package/dist/human/web.js +184 -0
- package/dist/human/worker.bundle.txt +3868 -0
- package/dist/human/worker.d.ts +1 -0
- package/dist/human/worker.js +81 -0
- package/dist/human/worlds.d.ts +24 -0
- package/dist/human/worlds.js +99 -0
- package/dist/mcp/agent.d.ts +10 -0
- package/dist/mcp/agent.js +78 -0
- package/dist/mcp/allow.d.ts +20 -0
- package/dist/mcp/allow.js +88 -0
- package/dist/mcp/http.d.ts +27 -0
- package/dist/mcp/http.js +92 -0
- package/dist/mcp/oauth.d.ts +91 -0
- package/dist/mcp/oauth.js +241 -0
- package/dist/mcp/route.d.ts +15 -0
- package/dist/mcp/route.js +42 -0
- package/dist/mcp/runner.d.ts +45 -0
- package/dist/mcp/runner.js +99 -0
- package/dist/mcp/server.d.ts +11 -0
- package/dist/mcp/server.js +102 -0
- package/dock/agents.ts +121 -0
- package/dock/apns.ts +138 -0
- package/dock/console.ts +23 -0
- package/dock/dialers.ts +85 -0
- package/dock/faculty.ts +148 -0
- package/dock/fcm.ts +103 -0
- package/dock/grants.ts +111 -0
- package/dock/http2.ts +70 -0
- package/dock/index.ts +133 -0
- package/dock/local.ts +48 -0
- package/dock/location.ts +22 -0
- package/dock/notifications.ts +67 -0
- package/dock/push.ts +161 -0
- package/dock/quo-app-bells.md +280 -0
- package/dock/quo-dock.md +1942 -0
- package/dock/service.ts +100 -0
- package/dock/timer.ts +174 -0
- package/dock/webpush.ts +182 -0
- package/harbor/browser.ts +97 -0
- package/harbor/capacitor.ts +223 -0
- package/harbor/disk.ts +237 -0
- package/harbor/edge/alarm.ts +48 -0
- package/harbor/edge/edge.ts +407 -0
- package/harbor/edge/exercise.ts +241 -0
- package/harbor/edge/given.ts +106 -0
- package/harbor/edge/platform.d.ts +39 -0
- package/harbor/edge/storage.ts +179 -0
- package/harbor/edge/text.d.ts +15 -0
- package/harbor/edge/worker.ts +70 -0
- package/harbor/edge/wrangler.toml +30 -0
- package/harbor/files.ts +221 -0
- package/harbor/http.ts +188 -0
- package/harbor/idb.ts +0 -0
- package/harbor/quo-harbor.md +850 -0
- package/harbor/quo.ts +44 -0
- package/harbor/resolve.ts +31 -0
- package/harbor/seal.ts +26 -0
- package/harbor/sealed.ts +183 -0
- package/harbor/tauri.ts +79 -0
- package/harbor/ward.ts +28 -0
- package/human/app.ts +580 -0
- package/human/apps.ts +47 -0
- package/human/dom.ts +21 -0
- package/human/door.ts +113 -0
- package/human/guest.ts +34 -0
- package/human/html.ts +490 -0
- package/human/local.ts +30 -0
- package/human/mark.ts +53 -0
- package/human/move.ts +122 -0
- package/human/quo-human.md +1042 -0
- package/human/screen.ts +136 -0
- package/human/style.ts +63 -0
- package/human/tab.bundle.txt +4775 -0
- package/human/tab.ts +446 -0
- package/human/tree.ts +217 -0
- package/human/wake.ts +191 -0
- package/human/web.ts +259 -0
- package/human/worker.bundle.txt +3868 -0
- package/human/worker.ts +124 -0
- package/human/worlds.ts +127 -0
- package/mcp/agent.ts +94 -0
- package/mcp/allow.ts +131 -0
- package/mcp/http.ts +92 -0
- package/mcp/oauth.ts +270 -0
- package/mcp/quo-mcp.md +343 -0
- package/mcp/route.ts +45 -0
- package/mcp/runner.ts +136 -0
- package/mcp/server.ts +101 -0
- package/package.json +121 -0
|
@@ -0,0 +1,850 @@
|
|
|
1
|
+
# Transports
|
|
2
|
+
|
|
3
|
+
This is how a harbor lives on a real device and how two of them meet over a real
|
|
4
|
+
network. It implements the harbor of `quo/SPEC.md` on every terrain and
|
|
5
|
+
adds nothing to it: a harbor boots wards, keeps what they are made of, carries
|
|
6
|
+
bytes to one pk, returns what came back or nothing, never reads them, and
|
|
7
|
+
answers nothing only where it knows nothing was sent. This document names no
|
|
8
|
+
being and no side; it knows seeds, partitions, code, sockets and disks.
|
|
9
|
+
|
|
10
|
+
## One harbor per device, one ward per world
|
|
11
|
+
|
|
12
|
+
A device runs one harbor. It is what the device already has, offered to Quo,
|
|
13
|
+
and there is one of it because a device has one keychain, one disk and one
|
|
14
|
+
set of processes. Two harbors on one device are a curiosity, not a design.
|
|
15
|
+
|
|
16
|
+
A harbor hosts many wards, and a ward is a world: a seed, a partition and a
|
|
17
|
+
body of code, with an origin. The wards of the device's owner and wards
|
|
18
|
+
whose code came from a stranger are hosted the same way. A native app is
|
|
19
|
+
therefore a browser of worlds: one harbor, a ward per world, each in its own
|
|
20
|
+
runtime, and the screen showing one at a time.
|
|
21
|
+
|
|
22
|
+
A ward is whole only where all three of its parts are. The harbor keeps a
|
|
23
|
+
**ward record** per ward, and it is the harbor's, never the ward's:
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
ward record
|
|
27
|
+
pk derived from the seed; the key of the record
|
|
28
|
+
seed where it is, in the secret store
|
|
29
|
+
partition where it is, in the data store
|
|
30
|
+
code where the class bodies come from, and which version boots
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
One of those wards is the harbor's own, the **dock ward**. A harbor with
|
|
34
|
+
anything to lend boots it and keeps its ask pointer instead of handing it
|
|
35
|
+
out, so it is that ward's root, and every faculty of the box is a being in
|
|
36
|
+
it. It is a ward in every other respect: a seed the store keeps, a
|
|
37
|
+
partition that persists, a door judging the thirteen cases. Two things are
|
|
38
|
+
its own. It is never adopted or dropped as a migration, because its beings
|
|
39
|
+
are the box's and die with it. And the harbor cleans up after a ward that
|
|
40
|
+
leaves: it is the root, so when it drops or hands out a ward it removes the
|
|
41
|
+
relations that ward's beings held at faculties, since a faculty's occupant
|
|
42
|
+
rows are not bounded the way a knock record is. A faculty cleans too, from
|
|
43
|
+
her own end, dropping a subscriber she has heard `unreached` from long
|
|
44
|
+
enough; the two catch different failures and neither is redundant.
|
|
45
|
+
|
|
46
|
+
`lend` is the last member of the ground, and it is built per ward: given
|
|
47
|
+
a name, the harbor asks its own root to invite on that faculty and hands
|
|
48
|
+
back the invitation, or null. Which ward may ask for which name is the
|
|
49
|
+
harbor's own decision, taken where the ground is built, and a ward whose
|
|
50
|
+
code came from a stranger is lent nothing. The ground is per ward and per
|
|
51
|
+
harbor, so `lend` reaches this box and no other; a ward that migrates is
|
|
52
|
+
handed the new harbor's, which is why a being asks again at every birth.
|
|
53
|
+
|
|
54
|
+
A harbor need not announce the dock ward's pk on the lines it dials. An
|
|
55
|
+
announce is a claim it chooses to make, and nothing is lost by withholding
|
|
56
|
+
this one: the only way through that door is a heir a faculty minted, and
|
|
57
|
+
the only source of one is `lend`.
|
|
58
|
+
|
|
59
|
+
## Three parts, three native places
|
|
60
|
+
|
|
61
|
+
The harbor adapts nothing. Every terrain already has a place for secrets, a
|
|
62
|
+
place for data and a place for code, and those are the three the harbor
|
|
63
|
+
needs:
|
|
64
|
+
|
|
65
|
+
| a ward is | Mac, iPhone | droplet | edge | browser tab |
|
|
66
|
+
| ----------- | --------------------- | -------------------------- | ----------------------- | ---------------------- |
|
|
67
|
+
| seed | a file, sealed | `.env` or a secrets file | the platform's secrets | IndexedDB, per origin |
|
|
68
|
+
| partition | a file, sealed | a file on disk | Durable Object storage | IndexedDB, per origin |
|
|
69
|
+
| the key | Keychain | the environment, if any | the platform's secrets | none |
|
|
70
|
+
| code | the app bundle | a folder | the deployed worker | a bundle URL |
|
|
71
|
+
|
|
72
|
+
Envoys read their own secrets from the same secret store as the seed. A
|
|
73
|
+
device has one place for secrets and the harbor is one more user of it.
|
|
74
|
+
|
|
75
|
+
The browser column is the honest weakness: a tab has no secret store for
|
|
76
|
+
bytes, so the seed sits beside the partition, guarded by the origin and
|
|
77
|
+
nothing more. That is the price of a tab, and why a tab is a rented room and
|
|
78
|
+
a native app is not.
|
|
79
|
+
|
|
80
|
+
### The partition, split or wrapped
|
|
81
|
+
|
|
82
|
+
Inside the partition one branch is secret, the bind table: each standing's
|
|
83
|
+
current and next keys, and the heirs minted and not yet spoken. Everything
|
|
84
|
+
else is data. That is why a side never holds one. What a harbor hands a side
|
|
85
|
+
for a ward it serves is `Hosted`: the door and the ask, the pk, the name, the
|
|
86
|
+
record, a save, and the beings it made, by key and as a list of keys. No
|
|
87
|
+
partition. A side that wants to know which beings a ward has, or which one it
|
|
88
|
+
marks public, asks the ward and reads the describe; `harbor/ward.ts` is that
|
|
89
|
+
read, `beingsOf` and `publicKey`, and every side here uses it. Asking costs
|
|
90
|
+
nothing to write: a ward that wrote nothing has nothing to save.
|
|
91
|
+
|
|
92
|
+
The partition is values only and the harbor may persist it as
|
|
93
|
+
it likes, so it may keep the secret branch apart from the data, by terrain:
|
|
94
|
+
|
|
95
|
+
- **Split.** The bind branch goes to the secret store beside the seed, the
|
|
96
|
+
rest to the data store. Fits a phone with a few relations; keychain items
|
|
97
|
+
are small and a being with a thousand standings has a thousand key pairs.
|
|
98
|
+
- **Wrapped.** The secret store holds one key per harbor; the data store
|
|
99
|
+
holds each ward whole, seed, partition and record as one blob encrypted
|
|
100
|
+
under it. Any size, one secret. The droplet and edge answer, and it
|
|
101
|
+
works on the phone too.
|
|
102
|
+
|
|
103
|
+
The ward never knows any of this. It hands values and gets values back, and
|
|
104
|
+
split or wrapped, one store or two, is the terrain's business. The one thing
|
|
105
|
+
it learns is yes or no: whether what it wrote was kept. A heir given away as
|
|
106
|
+
an invitation is a live credential until it speaks, and on a device other
|
|
107
|
+
apps share that is the argument for the secret store.
|
|
108
|
+
|
|
109
|
+
Wrapped is what is built, and it is one piece, `seal.ts`: AES-GCM under a
|
|
110
|
+
32-byte key, a fresh nonce each time, nonce and ciphertext together as hex,
|
|
111
|
+
on WebCrypto so it runs wherever a store does. The key is the one secret a
|
|
112
|
+
harbor reads from its terrain's secret store, named `QUO_SEED_KEY` on every
|
|
113
|
+
terrain: a platform secret on the edge, the environment of a daemon on a
|
|
114
|
+
device, handed to it from the Keychain by the app that spawned it. A
|
|
115
|
+
droplet gives no key and its folder stays plain under its file modes. A
|
|
116
|
+
store speaks one form, the one its key decides, and refuses a ward kept in
|
|
117
|
+
the other by name, so a daemon started without its key, or with one
|
|
118
|
+
against a plain folder, fails loudly and boots nothing. Sealing a plain
|
|
119
|
+
folder is a deliberate operator command, never a boot's doing.
|
|
120
|
+
|
|
121
|
+
### A save the store refuses
|
|
122
|
+
|
|
123
|
+
A save is part of the ask that caused it. At the end of every arrival, after
|
|
124
|
+
the being has answered and before the ward seals the reply, the ward asks the
|
|
125
|
+
harbor to keep what it wrote. The harbor saves and says whether the store
|
|
126
|
+
took it. That is the last moment anything can still be said: a reply sealed
|
|
127
|
+
to an asker's lid cannot be unsaid, and only the ward can seal.
|
|
128
|
+
|
|
129
|
+
A store that refused makes that arrival a failed ask, said with the words a
|
|
130
|
+
failed ask already has, `threw` to a key the door holds and silence to
|
|
131
|
+
everyone else. Never a reason. A full disk is an insides of this device, and
|
|
132
|
+
the far side hears no more of it than it hears of anything else in here.
|
|
133
|
+
|
|
134
|
+
A row the store would not take leaves the being standing at what is kept. The
|
|
135
|
+
harbor knows what that is without asking anybody: it keeps the copy it handed
|
|
136
|
+
the last save that succeeded, row by row, and puts the refused row back to it
|
|
137
|
+
in the ward's memory, in place and behind the cells guard. Her next ask sees
|
|
138
|
+
what the device sees and a smaller write after it is taken. She loses the
|
|
139
|
+
writes that were refused, which is exactly what their askers were told. The
|
|
140
|
+
row is written in place because a booted being holds her cells and her bind
|
|
141
|
+
table by reference: a fresh object in their place would strand her on cells
|
|
142
|
+
nothing keeps. A row the last successful save dropped is a being the store no
|
|
143
|
+
longer keeps, and her row goes with it.
|
|
144
|
+
|
|
145
|
+
The harbor never asks a store what it holds to find that out. The one moment
|
|
146
|
+
it would ask is from inside a save the store has just refused, which is the
|
|
147
|
+
read least likely to be true: the edge answers it with a partition missing its
|
|
148
|
+
head row, and a ward put back from that loses its heirs and answers its own
|
|
149
|
+
occupants with silence. What the copy costs is one copy of the rows of each
|
|
150
|
+
save, taken before the store is asked and kept only if the store takes it,
|
|
151
|
+
plus one copy of the partition when a ward is hosted, so that a being whose
|
|
152
|
+
first write of the run is refused is put back too. No store is read and no
|
|
153
|
+
extra write is made.
|
|
154
|
+
|
|
155
|
+
The fault is counted too, and a box says how many saves it refused wherever a
|
|
156
|
+
box says anything about itself: `/health` on the daemon and on the edge
|
|
157
|
+
carries the count per ward, and a daemon journals one line the first time a
|
|
158
|
+
ward's save is refused. A counter nobody reads is the thing this replaces.
|
|
159
|
+
A harbor that keeps nothing is asked nothing, and a ward running over no
|
|
160
|
+
store answers as though none of this were here. A store that refuses every
|
|
161
|
+
save cannot keep a ward's first write either, so no ward stands on it at all.
|
|
162
|
+
|
|
163
|
+
The reason this is worth a member of the ground: on a terrain where a being
|
|
164
|
+
is one row with a ceiling, a refused save is not a rare disk fault. Past the
|
|
165
|
+
ceiling every save of that being is refused, her memory runs ahead of what is
|
|
166
|
+
kept, and everything written to her afterwards is lost at the next reload
|
|
167
|
+
while every door goes on answering success. A door that says yes to what was
|
|
168
|
+
never kept is the worse fault, and it is the one a counter cannot fix.
|
|
169
|
+
|
|
170
|
+
### A being's row has a ceiling
|
|
171
|
+
|
|
172
|
+
A being is one row, and every store that keeps a row as one value has a
|
|
173
|
+
ceiling on it. The store refuses the row itself, before the platform is
|
|
174
|
+
asked, and it reads the ceiling from one place beside the rest of what that
|
|
175
|
+
terrain is: two stores disagreeing about the limit is a row kept the day
|
|
176
|
+
before and refused the day after with nothing in the tree saying why. Where a
|
|
177
|
+
terrain publishes no number, its store has no ceiling of its own and a full
|
|
178
|
+
disk is what refuses.
|
|
179
|
+
|
|
180
|
+
The edge is the terrain that publishes one. A SQLite-backed Durable Object
|
|
181
|
+
takes a key and a value of 2 MB together, which is `ROW_CEILING` in
|
|
182
|
+
`harbor/edge/given.ts`, and a being whose cells and bind table pass it is
|
|
183
|
+
refused by `DurableStorage` rather than by the platform. What lives beside a
|
|
184
|
+
ward for values a partition cannot hold is a design of its own: a partition
|
|
185
|
+
holds values, and a holiday video is not one.
|
|
186
|
+
|
|
187
|
+
### Custody is a lease
|
|
188
|
+
|
|
189
|
+
`quo/SPEC.md` makes custody the harbor's vouch: two harbors over one
|
|
190
|
+
seed are two wards with one pk, and a harbor refuses to boot a ward another
|
|
191
|
+
running harbor holds. The dock makes that a property of the store, not a rule
|
|
192
|
+
people follow: a lease beside the seed in the secret store, taken by the daemon
|
|
193
|
+
that boots the ward, checked before any boot, refused while held. A restored
|
|
194
|
+
backup meets a held lease and does not boot. A second tab on one origin meets a
|
|
195
|
+
lock and is the held page, `papers/quo-human.md` says which, and so does the
|
|
196
|
+
service worker on that origin meet it: it is
|
|
197
|
+
one more claimant of the same lock and holds the harbor only when no page does.
|
|
198
|
+
The edge is a Durable Object, which is the lease built in. None of it stops
|
|
199
|
+
a deliberate copy, and nothing can; it stops every accident, and accidents
|
|
200
|
+
are the whole population of this problem.
|
|
201
|
+
|
|
202
|
+
### Migration is three moves
|
|
203
|
+
|
|
204
|
+
A ward moves between harbors by moving its three parts: the seed from one
|
|
205
|
+
secret store to another, the partition from one data store to another, and
|
|
206
|
+
the new harbor pointed at the same code. Same seed, same pk, every standing
|
|
207
|
+
anyone holds still points at her. The old harbor deletes its copies first:
|
|
208
|
+
two harbors holding one seed are two wards with one pk, diverging silently.
|
|
209
|
+
|
|
210
|
+
What a move costs is not the three parts, which are bytes, but how many
|
|
211
|
+
peers wrote down where the ward was. A pk is permanent by arithmetic; a
|
|
212
|
+
location is soft state in every other harbor's directory, and only that goes
|
|
213
|
+
stale. So the bill is proportional to who hard-coded it, and there are two
|
|
214
|
+
kinds of ward.
|
|
215
|
+
|
|
216
|
+
A ward whose harbor dials out is free. It announces what it holds when a
|
|
217
|
+
line opens and whenever that changes, the listener proves each claim at the
|
|
218
|
+
door and binds it, and a ward that arrives on a new box is reachable as soon
|
|
219
|
+
as its harbor dials. Nobody is told and no peer acts.
|
|
220
|
+
|
|
221
|
+
A ward that is dialed keeps a second name. Peers hold a hint, pk at a URL,
|
|
222
|
+
in their own stores and across their own restarts, so that URL is a promise
|
|
223
|
+
the same way the pk is. Moving boxes does not break it, because a hostname
|
|
224
|
+
is not a location: DNS and the proxy point it wherever the harbor now runs,
|
|
225
|
+
which is the whole reason the route is a hostname and not an address.
|
|
226
|
+
Changing the URL itself is the move that costs, and it cannot be announced
|
|
227
|
+
to a peer this ward cannot reach.
|
|
228
|
+
|
|
229
|
+
When it must change, what is republished is a hint and never an invitation.
|
|
230
|
+
The relation, its keys and its count are untouched, and the invitation is
|
|
231
|
+
as good as it was: that is why the two travel separately, a link being one
|
|
232
|
+
next to the other. Peers on a rendezvous this ward also dials relearn by
|
|
233
|
+
themselves, since a binding is proven at the door and names no URL; peers
|
|
234
|
+
holding a hint need a new one, and a hint is not a capability, so it may be
|
|
235
|
+
published in the open to all of them at once. Nobody is invited twice.
|
|
236
|
+
|
|
237
|
+
The rule, then: a dialable ward keeps two names and owes the second the same
|
|
238
|
+
care as the first, and a ward that only dials keeps one, which is why a
|
|
239
|
+
laptop, a tab and a phone move home without ceremony and the droplet does
|
|
240
|
+
not.
|
|
241
|
+
|
|
242
|
+
### Code is a harbor decision
|
|
243
|
+
|
|
244
|
+
A ward's code has an origin: a folder the owner wrote, a bundle a world
|
|
245
|
+
published, a worker deployed. When the origin ships new bodies the harbor
|
|
246
|
+
decides whether the ward reboots on them. A reboot is silent, the ward cannot
|
|
247
|
+
tell old bodies from new, and a body the cells were not written for is an
|
|
248
|
+
absence this run. So the harbor pins what a ward boots on, records it in the
|
|
249
|
+
ward record, and moves it deliberately: on its own for the owner's own code,
|
|
250
|
+
with the owner's word for a world's.
|
|
251
|
+
|
|
252
|
+
## The daemon, and the two cores
|
|
253
|
+
|
|
254
|
+
There are two harbor cores, and every device runs one of them:
|
|
255
|
+
|
|
256
|
+
- **The node daemon**, `quo serve`. A disk or keychain store, a unix socket
|
|
257
|
+
for the owner, listeners on localhost for the routes. Droplet, Mac, Linux
|
|
258
|
+
box: wherever a home runs.
|
|
259
|
+
- **The browser harbor.** A dialer inside a webview or a tab, an IndexedDB
|
|
260
|
+
store or a sealed store handed in, one socket per world it is connected
|
|
261
|
+
to. A tab, and every app, phone and desk alike, since an app is a
|
|
262
|
+
presence and a presence dials.
|
|
263
|
+
|
|
264
|
+
The two cores meet a ward they cannot open differently, and both are right.
|
|
265
|
+
The core carries on and names it in `unbooted`, because a device with many
|
|
266
|
+
worlds must not lose them all to one. The daemon stops there and says which
|
|
267
|
+
ward and why: a daemon's folder is an operator's, every ward in it is meant
|
|
268
|
+
to be served, and coming up quietly with one missing is that world gone dark
|
|
269
|
+
with nobody told. It is the same rule that makes a sealed folder with no key
|
|
270
|
+
loud rather than a daemon serving nothing.
|
|
271
|
+
|
|
272
|
+
Tauri and Capacitor are packaging, not harbors. Both apps ship the browser
|
|
273
|
+
harbor over a native sealed store, the phone with push and the desk without
|
|
274
|
+
it, and each is its own owner for its one harbor. Neither ships the daemon:
|
|
275
|
+
a home runs on a box that runs day and night, and a laptop is a presence
|
|
276
|
+
like a phone. The edge is the daemon's core over a Durable Object, one
|
|
277
|
+
single-writer object per ward, or it diverges.
|
|
278
|
+
|
|
279
|
+
A wrapped app carries its own bundle and never loads it from a hostname.
|
|
280
|
+
Only a plain tab fetches the bundle from a world's `web.`, because it has
|
|
281
|
+
nowhere else to get it. A wrapped app visits a world's `web.` exactly once,
|
|
282
|
+
in a system browser sheet, to do the credential exchange and come back with
|
|
283
|
+
an invitation; after that it speaks to that world's `quo.` for bytes and
|
|
284
|
+
never to its `web.` again. One app on a device meets many worlds, one ward
|
|
285
|
+
each, and there is never an app per world.
|
|
286
|
+
|
|
287
|
+
## Listeners and dialers
|
|
288
|
+
|
|
289
|
+
Two kinds of harbor exist in the world, and every pair reduces to them:
|
|
290
|
+
|
|
291
|
+
| terrain | can be dialed | can dial | lifetime | wakes on |
|
|
292
|
+
| ------------ | ------------- | -------- | ----------------------- | -------- |
|
|
293
|
+
| droplet | yes | yes | as long as wanted | itself |
|
|
294
|
+
| edge | yes | fetch | per request | request |
|
|
295
|
+
| desktop app | no | yes | while running | itself |
|
|
296
|
+
| phone app | no | yes | foreground, throttled | push |
|
|
297
|
+
| browser tab | no | yes | while open | itself |
|
|
298
|
+
|
|
299
|
+
A ward on a dialer is reached only if its harbor dialed first and something
|
|
300
|
+
holds that line open. A phone's ask must survive a push wake-up, and a
|
|
301
|
+
device that comes back from sleep tells its dialer so, `wake`, since the
|
|
302
|
+
system closes a sleeping socket without a word; `quo/SPEC.md` says
|
|
303
|
+
what the dialer does with it. A tab is a
|
|
304
|
+
device, and one seed never lives in two tabs.
|
|
305
|
+
|
|
306
|
+
## Transport by necessity
|
|
307
|
+
|
|
308
|
+
Transport is chosen by necessity and nothing else. Two wards in one harbor:
|
|
309
|
+
no transport, the harbor calls its other door. Both harbors can listen: a
|
|
310
|
+
request, plain HTTP or raw TCP, and a reverse ask is a request the other
|
|
311
|
+
way. One of them cannot listen, behind NAT, in a tab, on a phone: a socket,
|
|
312
|
+
opened by the one that can dial and held by the one that can listen, with
|
|
313
|
+
both sides sending asks over it and a frame id matching replies. There is no
|
|
314
|
+
fourth situation, so there is no fourth reach, and none is ever picked
|
|
315
|
+
because it is nice. A being cannot tell which one carried her ask, and that
|
|
316
|
+
is the proof the choice was free.
|
|
317
|
+
|
|
318
|
+
A connection is a reach, never a harbor, and a ward outlives it. When a
|
|
319
|
+
socket drops, the listener's directory entry for that pk goes with it, the
|
|
320
|
+
listener answers nothing, the dialer's harbor turns that into unreached, and
|
|
321
|
+
the relation is untouched: seed, partition and keys are on the dialer, not on
|
|
322
|
+
the wire. When the dialer reconnects it announces its pks, the listener
|
|
323
|
+
probes each one at its door and rebinds what proved, and the next ask goes
|
|
324
|
+
through under the same keys and the same count. A phone in a tunnel loses
|
|
325
|
+
signal, never identity.
|
|
326
|
+
|
|
327
|
+
Untouched is the relation and not her record of it. An ask that reached
|
|
328
|
+
nobody has still taken its number and staged the key it announced, because
|
|
329
|
+
she must never offer one number twice; the count carries the gap and the far
|
|
330
|
+
door honours any number above its mark. So a tunnel costs a number and a
|
|
331
|
+
rotation, and nothing else.
|
|
332
|
+
|
|
333
|
+
## Reach, directory, rendezvous
|
|
334
|
+
|
|
335
|
+
A harbor needs two reach kinds and nothing more, which is what
|
|
336
|
+
`quo/SPEC.md` says and what `packages/quo/src/harbor/reach.ts` is:
|
|
337
|
+
|
|
338
|
+
- **request**: a URL. Send bytes, get bytes. Listener to listener.
|
|
339
|
+
- **socket**: a held connection, opened by whichever side can dial, used in
|
|
340
|
+
both directions with a frame id. A WebSocket, a WebRTC channel and a
|
|
341
|
+
native socket are one socket reach.
|
|
342
|
+
|
|
343
|
+
Two runtimes on one device are no third kind. Either they are one harbor,
|
|
344
|
+
and it calls its other door with no transport at all, or they are two
|
|
345
|
+
harbors, and one of them dials the other as any dialer does.
|
|
346
|
+
|
|
347
|
+
Plus one forwarding rule: a pk whose reach is another harbor that holds a
|
|
348
|
+
socket to it. That rule is the **rendezvous**, and with it every dialer is
|
|
349
|
+
reachable: a phone holds one socket to `quo.acme.com`, acme's harbor binds
|
|
350
|
+
the phone's pk to that socket, and a third harbor sending to the phone's pk
|
|
351
|
+
sends to acme, which forwards the same bytes to the same pk. Nobody changes
|
|
352
|
+
a pk or a byte, and an intermediary opens nothing.
|
|
353
|
+
|
|
354
|
+
What a box can see is the wire and only the wire. Its `/health` shows every
|
|
355
|
+
foreign pk and the socket it holds it on, and one phone is one harbor
|
|
356
|
+
holding one socket, so a phone that joined two worlds hosted on that box
|
|
357
|
+
announces both wards together: that is the box hosting both worlds, and
|
|
358
|
+
two worlds on two estates share no socket and have nothing to correlate.
|
|
359
|
+
No being of either world sees any of it, since an occupant sees pks never.
|
|
360
|
+
|
|
361
|
+
What it does learn is worth writing down, because "pks and ciphertext" is
|
|
362
|
+
true and reads as a smaller thing than it is. A harbor forwarding for
|
|
363
|
+
someone else holds, for every ask it carries: the destination ward pk, in
|
|
364
|
+
the clear, because that is the address and there is no forwarding without
|
|
365
|
+
it; the socket it came in on, and so the dialer's own address; the size of
|
|
366
|
+
the ask and of the reply, which are not padded and so move with the
|
|
367
|
+
plaintext; the time each crossed, and which reply belongs to which ask,
|
|
368
|
+
because it awaits the one to answer the other; and, from the announce
|
|
369
|
+
frame, which ward pks are held by one device. So an operator of a
|
|
370
|
+
rendezvous can keep who spoke to whom, when, and how much, for as long as
|
|
371
|
+
it likes, and break no ciphertext to do it. What it cannot learn is the
|
|
372
|
+
relation: the heir is sealed inside the ask, so one standing at a ward
|
|
373
|
+
cannot be told from another, nor followed from one rendezvous to the next.
|
|
374
|
+
Being reachable through someone costs the fact that you spoke. It does not
|
|
375
|
+
cost what you said, nor who you are to the far being.
|
|
376
|
+
|
|
377
|
+
A rendezvous is a listener and nothing more, so there is never one of them.
|
|
378
|
+
A harbor may hold sockets to several and be reached through any: the
|
|
379
|
+
directory is keyed by pk and a bind is per socket, so each listener binds
|
|
380
|
+
the same pks independently. Plurality is the design and not a deployment
|
|
381
|
+
habit, and a world that can only be reached through one listener chose
|
|
382
|
+
that.
|
|
383
|
+
|
|
384
|
+
A line also says which **wire suite** it speaks, one number that is not
|
|
385
|
+
negotiated: in the text frame a socket announces itself with, and as the
|
|
386
|
+
header `quo-suite` on a request. It is never on an ask, so an ask is bytes
|
|
387
|
+
with nothing in front of them, and a harbor still knows before it carries
|
|
388
|
+
anything whether the far side can open what it would send. A side meeting a
|
|
389
|
+
suite it does not know closes the line; a request door answers nothing
|
|
390
|
+
delivered. `quo/SPEC.md` owns the number and says why it exists.
|
|
391
|
+
|
|
392
|
+
The **directory** is the map from pk to reach. The memory harbor stubs it with
|
|
393
|
+
linked peers. A real one is a table the harbor fills however it likes, and the
|
|
394
|
+
one entry it must learn by itself is a dialer's: when a dialer connects it
|
|
395
|
+
announces the ward pks it holds, and the listener binds to that socket each one
|
|
396
|
+
whose door answered its probe; `quo/SPEC.md` says what the probe is.
|
|
397
|
+
After a migration the pk is unchanged, only its reach, and the next connect
|
|
398
|
+
rebinds it.
|
|
399
|
+
|
|
400
|
+
## The quo. route, as built
|
|
401
|
+
|
|
402
|
+
The reach interface and its two implementations are the library's,
|
|
403
|
+
`packages/quo/src/harbor/reach.ts`, and pass its reach suite here: the request
|
|
404
|
+
and a socket to a daemon in `packages/dock/test/wire.test.ts`, the request
|
|
405
|
+
through the worker inside the edge. The route is one file on every terrain,
|
|
406
|
+
`packages/dock/harbor/quo.ts`, `Request` in and `Response` out, mounted by
|
|
407
|
+
the daemon under `/quo` and by the worker under its harbor's prefix; it asks
|
|
408
|
+
its harbor for one thing, an ask delivered to a pk. The listener half is the
|
|
409
|
+
terrain's own and is not in it: `ws` at the daemon's door in
|
|
410
|
+
`packages/dock/harbor/http.ts`, a socket pair in the edge harbor.
|
|
411
|
+
|
|
412
|
+
```
|
|
413
|
+
request POST <url>/<pk>, the sealed ask as the body, the sealed reply as the answer;
|
|
414
|
+
<url> is the world's quo. route, which the proxy maps onto the daemon's /quo
|
|
415
|
+
404 is "no reach for that pk" and comes back as nothing; a line lost after
|
|
416
|
+
sending answers nothing at all, and the ward's bound ends the ask
|
|
417
|
+
at most 1 MiB is read, and a body over it is answered by that same 404: an
|
|
418
|
+
ask too big for this door is nothing delivered, never a silence, so it is
|
|
419
|
+
safe to send again and it will fail again. The number is this daemon's and
|
|
420
|
+
is on no wire; a caller cannot ask for it and is never told which of the
|
|
421
|
+
two a 404 was
|
|
422
|
+
socket a WebSocket at <url>, held by the dialer, used both ways
|
|
423
|
+
text frame { announce: [pk, ...] } the ward pks this side holds
|
|
424
|
+
ask frame [0][id 4][pk 64][bytes] a frame id matches the reply
|
|
425
|
+
reply frame [1][id 4][bytes]
|
|
426
|
+
none frame [2][id 4] nothing was delivered
|
|
427
|
+
directory pk -> reach, filled three ways: a socket a dialer holds, bound once proven
|
|
428
|
+
and unbound at close; a hint from a link, `quo reach <pk> <url>`, kept in
|
|
429
|
+
<dir>/reach.json; and the harbor's own doors, always first
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
One rule makes the rendezvous: bytes that arrive from the wire go to an own
|
|
433
|
+
door or to a socket this harbor holds for that pk, and never onward by
|
|
434
|
+
request. So a third harbor with a hint that a phone's pk is at acme's
|
|
435
|
+
`quo.` sends there, and acme forwards down the socket the phone holds.
|
|
436
|
+
A row on the dock ward's `dialers` faculty makes a daemon a dialer, placed
|
|
437
|
+
once by the root, `quo ask --ward dock dialers keep '{"url":"https://quo.acme.com"}'`,
|
|
438
|
+
kept in her cells and dialed again at every boot: it holds one socket per
|
|
439
|
+
URL, announces its wards on open, binds the listener's pks, and reconnects
|
|
440
|
+
with backoff from a second to thirty when the line drops. `drop` closes
|
|
441
|
+
the line and forgets it. A pk is 64 bytes on the wire, the ward key and
|
|
442
|
+
the padlock.
|
|
443
|
+
|
|
444
|
+
The proof is `packages/dock/test/wire.test.ts`: the library's conformance suite,
|
|
445
|
+
untouched, against two disk harbors in two folders with HTTP doors on
|
|
446
|
+
loopback, once with every ask a request and once with the second harbor
|
|
447
|
+
dialing the first; down is the reach refused, migrate is the ward folder
|
|
448
|
+
moved and adopted. Then the rendezvous on its own.
|
|
449
|
+
|
|
450
|
+
Two rules the daemon obeys and a reader would otherwise rediscover: a harbor
|
|
451
|
+
writes the partition before it lets a folder go, because a being driven
|
|
452
|
+
in-process changes it without passing a door; and a proxy sends the route
|
|
453
|
+
root as `/quo/`, with the trailing slash, which the door takes.
|
|
454
|
+
|
|
455
|
+
## The two cores, as built
|
|
456
|
+
|
|
457
|
+
The harbor core is the library's, `packages/quo/src/harbor/core.ts`, and so is
|
|
458
|
+
the dialer, `packages/quo/src/harbor/dial.ts`; `quo/SPEC.md` says what
|
|
459
|
+
they are. Every harbor here extends the core and hands it three things: a
|
|
460
|
+
**store**, the library's interface, with `files.ts` on a disk, plain as three
|
|
461
|
+
files per ward or sealed as one blob when the daemon holds `QUO_SEED_KEY`,
|
|
462
|
+
and `idb.ts` in a tab, each passing the library's store suite, the files
|
|
463
|
+
store in both forms; a **loader**, the code half, a
|
|
464
|
+
module from a folder on a daemon and the bundle in a tab; and a **lease**, a pid
|
|
465
|
+
file on disk and a web lock on the database name in a tab, so a second tab on
|
|
466
|
+
one origin meets the lock and is the held page. The disk harbor, `disk.ts`, is the
|
|
467
|
+
core plus files, the loader and the pid, and every socket dialed to it is held
|
|
468
|
+
by its HTTP door, which is where this terrain's `ws` listener stands; the
|
|
469
|
+
browser harbor, `browser.ts`, is the core plus
|
|
470
|
+
IndexedDB, the built-in beings, the lock and one dialer per world.
|
|
471
|
+
|
|
472
|
+
The harbor folder is the device's and not a package: it has no node_modules
|
|
473
|
+
and is not meant to, and a class file in it imports the library by name like
|
|
474
|
+
any code. So the disk harbor resolves a class file's packages from where the
|
|
475
|
+
dock itself is installed, `resolve.ts`, a module resolution hook registered
|
|
476
|
+
once before the first class loads: a bare name that does not resolve from a
|
|
477
|
+
file outside any node_modules is resolved again from the dock's own place.
|
|
478
|
+
A class sees the packages the daemon sees, the library and the dock's own,
|
|
479
|
+
and one copy of each, so a `Being` in a class file is the daemon's `Being`.
|
|
480
|
+
|
|
481
|
+
A tab's store keeps values through JSON, as a file does, because the ward
|
|
482
|
+
hands its partition out through a guard that structured clone refuses.
|
|
483
|
+
The daemon's `/quo` and `/health` answer any origin, with the preflight a
|
|
484
|
+
binary POST needs, because a tab on one world's `web.` reaching another
|
|
485
|
+
world's `quo.` is the ordinary case.
|
|
486
|
+
|
|
487
|
+
`/health` asks for nothing and tells what this harbor hosts: the wards by
|
|
488
|
+
name and pk, the sockets held, the directory, and the saves its store
|
|
489
|
+
refused, per ward. That is a map of the
|
|
490
|
+
device, so it is an operator's surface and not a route: the proxy maps the
|
|
491
|
+
three hostnames onto their paths and nothing onto this one, and a request
|
|
492
|
+
for it from outside meets a 404 at the proxy. It answers any origin because
|
|
493
|
+
it is on loopback, and the day it is put behind a proxy it needs a reason
|
|
494
|
+
and a door.
|
|
495
|
+
|
|
496
|
+
The proof is `packages/dock/test/terrain/browser.test.ts`, behind
|
|
497
|
+
`npm run deep:dock` beside the library's own browser chapter: the
|
|
498
|
+
conformance suite, untouched, inside a real Chromium, against two browser
|
|
499
|
+
harbors in one tab with two databases, both dialing a Node daemon on
|
|
500
|
+
loopback, so every relation crosses the rendezvous; down is the reach
|
|
501
|
+
refused on both, migrate lifts seed, partition and record from one database
|
|
502
|
+
into the other and boots there, the tab-to-home scene. And the lock: a
|
|
503
|
+
second harbor over one database does not boot.
|
|
504
|
+
|
|
505
|
+
### One thing a terrain's fetch decides
|
|
506
|
+
|
|
507
|
+
APNs, the ring `dock/apns.ts` is, speaks HTTP/2 and refuses HTTP/1.1.
|
|
508
|
+
Node's `fetch` is undici, which has no HTTP/2 client, so a daemon on Node
|
|
509
|
+
rings Apple through `dock/http2.ts` instead, the same ring over
|
|
510
|
+
`node:http2`. A worker's fetch reaches Apple over HTTP/2 and so does
|
|
511
|
+
Deno's and Bun's, and those terrains use the `fetch` one. What the two
|
|
512
|
+
send is one function in `dock/apns.ts`, the path, the headers and the
|
|
513
|
+
body, because a phone rung two ways is rung the same way twice; what
|
|
514
|
+
differs is the transport and nothing else. The Node one is its own file
|
|
515
|
+
so that a worker bundle never pulls `node:http2` in, which is why nothing
|
|
516
|
+
terrain neutral names it and `dock/index.ts` does not export it. All of
|
|
517
|
+
this is here rather than in the being because a being names no terrain.
|
|
518
|
+
|
|
519
|
+
## The edge, as built
|
|
520
|
+
|
|
521
|
+
`packages/dock/harbor/edge/` is the core over one Durable Object, and the object
|
|
522
|
+
is the harbor. The platform runs one instance of it at a time, which is
|
|
523
|
+
the lease and the single writer every ward needs. Its storage is the
|
|
524
|
+
store, `storage.ts`: one row per ward, seed, partition and record, with the
|
|
525
|
+
seed sealed by `seal.ts` under a key from the platform's secrets,
|
|
526
|
+
`QUO_SEED_KEY`, so the storage holds ciphertext and the secret store holds
|
|
527
|
+
the one key, as the table above says. The deployed worker is the code, the
|
|
528
|
+
built-in beings and whatever it hands in. It is a listener and never a
|
|
529
|
+
dialer: reached by request at `<origin>/h/<name>/quo`, holding the sockets
|
|
530
|
+
dialers open to it on the platform's own socket pair, the rendezvous for
|
|
531
|
+
them, awake per request and kept awake by a held socket.
|
|
532
|
+
|
|
533
|
+
Its owner door is a route, not a socket, because the platform has no local
|
|
534
|
+
process: the root is whoever holds `QUO_ROOT`, a platform secret, and the
|
|
535
|
+
root's asks arrive as one POST at `<origin>/h/<name>/root` under it, the
|
|
536
|
+
first of them `{ init: { ward, user } }`, the root's setup. That is the
|
|
537
|
+
device's own rule on this terrain, as file permissions are on a disk. The
|
|
538
|
+
first thing a root on the edge does is invite an owner on the ward's pk,
|
|
539
|
+
and from then on the edge is piloted from elsewhere over a standing, with
|
|
540
|
+
`--via`, like any ward.
|
|
541
|
+
|
|
542
|
+
What a side mounted there is given is one shape, `given.ts`, and it arrives
|
|
543
|
+
in two halves because the halves change at different times. The deploy
|
|
544
|
+
carries the tab's bundle and the service worker's, as text modules a
|
|
545
|
+
`type = "Text"` rule in the `wrangler.toml` names, since a built artefact of
|
|
546
|
+
a deploy is not a row a ward keeps. What is worn or configured while the
|
|
547
|
+
worker runs is rows in the object's own storage, written by the root's door:
|
|
548
|
+
the public origins, the apps table and the bell key under `settings`, and the
|
|
549
|
+
design a world wears one row per file. So the four routes run there with
|
|
550
|
+
nothing of them changed. `<origin>/h/<name>/web/<ward>` is a world's page, and
|
|
551
|
+
the routes that page hands its tab are the worker's own until a root names
|
|
552
|
+
public ones; `<origin>/h/<name>/api` is a describe as JSON for a stranger or
|
|
553
|
+
a bearer, and `<origin>/h/<name>/mcp` is the dance and the endpoint, with the
|
|
554
|
+
dance's records kept in the same object under one key.
|
|
555
|
+
|
|
556
|
+
A worker stands a box ward, like every other terrain: the box's state has one
|
|
557
|
+
home everywhere, the dock ward's partition, and a terrain that cannot stand
|
|
558
|
+
that ward does not get a second place to keep box state. What differs by
|
|
559
|
+
terrain is which faculties the ward lends, and `given.ts` holds the edge's
|
|
560
|
+
list with the reason for each name that is missing. It lends two. `grants`,
|
|
561
|
+
the bearers its routes honour, which is a record in her cells and the same
|
|
562
|
+
code here as on a disk. And `timer`, on the object's own storage alarm:
|
|
563
|
+
`Alarm` in `edge/alarm.ts` is the box's timer with the one method that knows
|
|
564
|
+
a terrain replaced, since a `setTimeout` dies with the request and an alarm
|
|
565
|
+
does not. One harbor is one object and every ward of it lives there, so the
|
|
566
|
+
alarm wakes the whole harbor and a tick from the timer to a home's Clock is
|
|
567
|
+
in-process here as it is on a disk. The object's `alarm` handler opens the
|
|
568
|
+
harbor, boots every ward from storage and runs the round, so a home on the
|
|
569
|
+
edge with a sweep at three in the morning wakes for it with no request to
|
|
570
|
+
cause it.
|
|
571
|
+
|
|
572
|
+
The other two are not lent: no place and no screen, since a worker is at
|
|
573
|
+
neither; and no ring, since a ring is signed under this world's own web
|
|
574
|
+
origin and a worker learns its origin from the request and not at boot. A
|
|
575
|
+
faculty the platform has no answer for is not lent, rather than lent and
|
|
576
|
+
hollow.
|
|
577
|
+
|
|
578
|
+
Which ward a caller who names none is at is the terrain's word too. On a disk
|
|
579
|
+
it is the mark an operator wrote in the folder; at the edge it is the one
|
|
580
|
+
world the object holds, and an object holding several says nothing rather
|
|
581
|
+
than guessing.
|
|
582
|
+
|
|
583
|
+
The proof is `packages/dock/test/terrain/edge.test.ts`, behind
|
|
584
|
+
`npm run deep:dock`, in the platform's own runtime on loopback under
|
|
585
|
+
`wrangler dev`: the conformance suite, untouched, inside the platform
|
|
586
|
+
against two edge harbors in one object over two prefixed views of its
|
|
587
|
+
storage, reaching each other by request through the worker; the root door
|
|
588
|
+
refused without the secret and answering under it; the box configured after
|
|
589
|
+
it is deployed, its settings merged and answered and a design file worn and
|
|
590
|
+
taken off; a world's page served out of the deploy's bundles and those rows;
|
|
591
|
+
an invitation traded for a bearer at the program side, an ask through the
|
|
592
|
+
avatar answered with the same JSON a Node daemon answers, and the model side
|
|
593
|
+
challenging a caller with no bearer; a home keeping a one-shot for the next
|
|
594
|
+
minute and being woken for it with nothing asking, the fire written down at
|
|
595
|
+
the minute it named and read back a minute later; and a Node daemon
|
|
596
|
+
dialing the edge, its pk held there, reached by a third harbor through the
|
|
597
|
+
edge, unreached when the line drops. Two platform rules shape it: one object
|
|
598
|
+
may not touch another's storage, so the exercise's two harbors share one
|
|
599
|
+
object the way two tabs share a page; and the platform's crypto refuses a
|
|
600
|
+
Uint8Array over a shared buffer and wants a plain one.
|
|
601
|
+
|
|
602
|
+
The `ws` question the road asked is answered by this terrain: the socket
|
|
603
|
+
reach's framing and the dialer are written on the standard surface and
|
|
604
|
+
hold on every terrain; the listener half is the terrain's own, `ws` on
|
|
605
|
+
Node and the socket pair on the edge, and stays where the terrain is. What
|
|
606
|
+
the library takes, when it takes the reach, is the framing and the dialer.
|
|
607
|
+
|
|
608
|
+
## The phone, as built
|
|
609
|
+
|
|
610
|
+
`sealed.ts` is the store an app holds on a device in a hand, the phone and
|
|
611
|
+
the desk alike: one key per harbor in the platform's secret store, marked
|
|
612
|
+
this device only so it never travels in a backup, and one sealed file per
|
|
613
|
+
ward, seed, partition and record as one blob, in a folder no backup
|
|
614
|
+
copies. The seal is `seal.ts`, the same piece the daemon and the edge use.
|
|
615
|
+
Opening the store is where custody is decided, since a backup is the one
|
|
616
|
+
copy nobody makes on purpose: a key with no folder is a reinstall, and the
|
|
617
|
+
stale key is deleted; a folder with no key is a restore to another device,
|
|
618
|
+
and the unreadable files are deleted; either way the harbor starts fresh
|
|
619
|
+
and there is never a twin. What a platform hands it is two small seams,
|
|
620
|
+
files and secrets, and the rule is proven once on the two in memory,
|
|
621
|
+
`test/sealed.test.ts`. `capacitor.ts` hands the phone's, the Filesystem
|
|
622
|
+
plugin in the folder iCloud does not copy and a secure-storage plugin over
|
|
623
|
+
the Keychain. The app that holds the store is `app/`, which the dock does
|
|
624
|
+
not know. Its page is `app/phone/entry.ts`, terrain glue and nothing else:
|
|
625
|
+
this harbor, the dock's shell over it, and the link the platform hands the
|
|
626
|
+
app, as the launch URL and then on every link opened while it runs.
|
|
627
|
+
|
|
628
|
+
The harbor on a phone is the browser harbor, `browser.ts`, over that
|
|
629
|
+
store: the browser harbor takes a name, which is an IndexedDB database
|
|
630
|
+
under a web lock, or a store handed in, whose lease is the process that
|
|
631
|
+
holds it, since an app has one harbor and nothing else can open its
|
|
632
|
+
files. `nativeHarbor` in `capacitor.ts` is that harbor booted and armed:
|
|
633
|
+
the App plugin's foreground event tells it `wake`, which tells every
|
|
634
|
+
dialer, because a phone asleep loses its sockets silently and the wake is
|
|
635
|
+
what dials them back.
|
|
636
|
+
|
|
637
|
+
On Android the same store stands on the same two plugins: the key in the
|
|
638
|
+
Keystore-backed secure store, the files in the app's own data folder,
|
|
639
|
+
which the manifest excludes from backup with `allowBackup` false, so a
|
|
640
|
+
restore finds nothing there either.
|
|
641
|
+
|
|
642
|
+
The phone's faculties are its plugins, geolocation and local
|
|
643
|
+
notifications, and `capacitor.ts` is two being classes over them, booted
|
|
644
|
+
into the phone's dock ward and lent by name: a position as latitude,
|
|
645
|
+
longitude, accuracy and time, and a notification shown under a
|
|
646
|
+
title. The platform's permission prompt is the first ask's; the terrain
|
|
647
|
+
grants it ahead, since no script taps a prompt, with `simctl privacy` and a
|
|
648
|
+
set location on iOS, and with `pm grant` and a geo fix on Android.
|
|
649
|
+
|
|
650
|
+
On iOS the notifications faculty also plants the banner key, which is the
|
|
651
|
+
one thing a second process needs and cannot ask a ward for. It goes
|
|
652
|
+
through the same secure store the harbor keeps its own key in: an item
|
|
653
|
+
written there lands in the app's first keychain group, which is the group
|
|
654
|
+
the notification extension is in, so the two meet with nothing written to
|
|
655
|
+
make them meet. A plugin of the app's own could not carry it, because the
|
|
656
|
+
bridge builds the page's plugin headers before it hands control to the
|
|
657
|
+
app, and the list that registers one earlier is a file `cap sync`
|
|
658
|
+
rewrites.
|
|
659
|
+
|
|
660
|
+
A notification is scheduled with `isExactNotification` false, which is not
|
|
661
|
+
the plugin's default. A notification that wants an exact alarm and has no
|
|
662
|
+
permission for one opens the Alarms and reminders settings screen on
|
|
663
|
+
Android 12 and up, and the call never answers until a human comes back
|
|
664
|
+
from it, so the ask goes `late` and nothing says why. Ours is shown now
|
|
665
|
+
and waits for no clock, and the permission it would otherwise want is one
|
|
666
|
+
a store asks an alarm app to justify.
|
|
667
|
+
|
|
668
|
+
The phone's ear is one more function from the same file, `phoneEar`,
|
|
669
|
+
handed to the shell rather than to a class, since the token is the box's
|
|
670
|
+
and no being's; what the human reads after a wake is the Notifications
|
|
671
|
+
faculty's ask, made by the shell through the harbor's own pointer, and no
|
|
672
|
+
second body. The ear registers for
|
|
673
|
+
remote notifications and answers the token, `apns` on one platform and
|
|
674
|
+
`fcm` on the other, and it is never sprung: registering is what puts the
|
|
675
|
+
permission prompt up, and an app that asks the moment it opens is an app a
|
|
676
|
+
human refuses on principle, so it answers null until notifications are
|
|
677
|
+
already allowed, which they are on the phone's own screen. The token
|
|
678
|
+
arrives as an event rather than as an answer, so the registration is
|
|
679
|
+
awaited once and kept, and the shell asks again on every foreground
|
|
680
|
+
because the platform hands out a new one whenever it likes. Which of
|
|
681
|
+
Apple's two push hosts minted it is the build's word and not the token's:
|
|
682
|
+
`phone/build.ts` writes in whether this shell was built pointed at a
|
|
683
|
+
world, which is the same signal that decides the link claim, and a build
|
|
684
|
+
told nothing is the store build that speaks to the production host.
|
|
685
|
+
|
|
686
|
+
The proof is two tests behind `npm run deep:app` on one stage,
|
|
687
|
+
`packages/dock/test/terrain/phone.ts`: `ios.test.ts` inside the real app
|
|
688
|
+
in the iOS Simulator and `android.test.ts` inside it in the Android
|
|
689
|
+
emulator. Each runs the store suite, untouched, and the custody rule
|
|
690
|
+
against a real secret store and a real folder; the whole conformance
|
|
691
|
+
suite over two harbors on native stores, both dialing a daemon on the
|
|
692
|
+
Mac's loopback through the tab's own probe; the wake, the app sent
|
|
693
|
+
behind Settings and brought back, its dialer told and its socket held
|
|
694
|
+
again; and the phone joining the daemon's world on an invitation, then a
|
|
695
|
+
model on the daemon asking the user being where the phone is, reaching the
|
|
696
|
+
phone's being on the standing home holds and her own location faculty
|
|
697
|
+
behind it, and getting where the Simulator or the emulator was put.
|
|
698
|
+
The app is synced with the stage's
|
|
699
|
+
origin as its page, built with xcodebuild or gradle, installed fresh and
|
|
700
|
+
launched with simctl or adb; the
|
|
701
|
+
page loads the bundled exercise, `native.ts`, runs it and posts the list
|
|
702
|
+
back. The device names the Mac `localhost`, the Simulator because it
|
|
703
|
+
shares the Mac's network and the emulator because adb reverses the
|
|
704
|
+
stage's two ports, and it has to be that name: a webview grants WebCrypto
|
|
705
|
+
to a plain-http page only on `localhost`, the one origin it counts secure
|
|
706
|
+
without TLS. Through adb's reversed port a socket that Node dropped stays
|
|
707
|
+
open on the device's side, so the stage answers every request with the
|
|
708
|
+
connection closed and every fetch the page makes carries a timeout. Two
|
|
709
|
+
things the plugins taught, held in the store: the secure store keeps
|
|
710
|
+
JSON, so a value is read with the call that parses; and mkdir refuses a
|
|
711
|
+
folder that exists, recursive or not.
|
|
712
|
+
|
|
713
|
+
## The desk
|
|
714
|
+
|
|
715
|
+
The desk app is the phone's shell on a desk: the same browser harbor, over
|
|
716
|
+
the same sealed store, showing the same screen, and a presence like the
|
|
717
|
+
phone. `app/tauri/` is the Tauri project, and its Rust core holds three
|
|
718
|
+
small things and nothing else: the files under the app's own data folder,
|
|
719
|
+
which no backup copies, refusing a path that climbs out of it; the keys in
|
|
720
|
+
the OS keychain under the app's service name, this device only; and the
|
|
721
|
+
notification center. `tauri.ts` hands the first two to the sealed store as
|
|
722
|
+
its seams over the core's commands, and its notifications faculty asks
|
|
723
|
+
the third. The window runs `human/app.ts` over that harbor, one ward per
|
|
724
|
+
world, every world dialed, carries the dock's stylesheet in its bundle,
|
|
725
|
+
and tells the harbor it is back whenever the window comes to the front,
|
|
726
|
+
since a desk sleeps and wakes like a phone.
|
|
727
|
+
|
|
728
|
+
The desk's dock ward lends what a desk has: notifications through the
|
|
729
|
+
platform's notification center, and nothing else. Location is what the
|
|
730
|
+
platform gives, which on a desk is nothing, so it is not lent rather than
|
|
731
|
+
lent and dumb. No timer, because a desk holds presences and no home, so no
|
|
732
|
+
Clock lives on it and a Timer there would arm for nobody; a Timer stands
|
|
733
|
+
only on a box that hosts a home. It has no ear, since no store rings a
|
|
734
|
+
desk, so a desk is reached while the app is open and drains what it
|
|
735
|
+
missed when it opens. The one
|
|
736
|
+
window the desk app has is a surface over the DOM like the tab's, and the
|
|
737
|
+
screen runs in it: nothing of the desk app is a daemon, spawns one or
|
|
738
|
+
attaches to one. A person who wants a home on a Mac runs `quo serve` on it
|
|
739
|
+
as an operator, and that is a home like any other and no concern of the
|
|
740
|
+
app's.
|
|
741
|
+
|
|
742
|
+
The desk has no honest link. macOS verifies an associated domain for an
|
|
743
|
+
app on the store and Windows and Linux verify nothing, so a join link on a
|
|
744
|
+
desk opens the browser, and the way a world reaches the desk app is the
|
|
745
|
+
Courier: the tab offers, the desk app takes the offer typed or pasted in.
|
|
746
|
+
The store and its custody rule are proven on the two seams in memory, and
|
|
747
|
+
the desk's seams, the keychain and the folder, are proven live: the app
|
|
748
|
+
built with cargo, coming up cold on the Console on the operator's Mac.
|
|
749
|
+
|
|
750
|
+
## The link
|
|
751
|
+
|
|
752
|
+
An invitation carries the ward pk of the world that minted it, and not where
|
|
753
|
+
that pk lives. A harbor that has never heard of that world cannot carry the
|
|
754
|
+
first knock. So a link is an invitation next to a **reach hint**, the URL of
|
|
755
|
+
that world's `quo.` route. The being receives the invitation opaque, as
|
|
756
|
+
always; the hint goes to the directory. The link is single-use, because the
|
|
757
|
+
heir dies at the first knock, and whoever opens it first is bound.
|
|
758
|
+
|
|
759
|
+
## Routes on a daemon
|
|
760
|
+
|
|
761
|
+
```
|
|
762
|
+
device
|
|
763
|
+
quo serve one process, one harbor, the ask pointer
|
|
764
|
+
quo.sock owner asks; local only; never behind a proxy
|
|
765
|
+
side.sock local sides; local only; never behind a proxy
|
|
766
|
+
127.0.0.1:8787 the HTTP door, loopback only; routes mount by path
|
|
767
|
+
/health the wards it hosts, by name and pk, and the saves refused; harbor/http.ts
|
|
768
|
+
/mcp the model side; mcp/route.ts
|
|
769
|
+
/web the worlds' pages and the allow page; human/web.ts, mcp/allow.ts
|
|
770
|
+
/quo the socket door: request in, sockets held, the rendezvous; harbor/quo.ts
|
|
771
|
+
/api a describe as JSON, a stranger's or a bearer's; api/route.ts
|
|
772
|
+
reverse proxy
|
|
773
|
+
mcp.example.com -> 127.0.0.1:8787/mcp
|
|
774
|
+
web.example.com -> 127.0.0.1:8787/web
|
|
775
|
+
quo.example.com -> 127.0.0.1:8787/quo
|
|
776
|
+
api.example.com -> 127.0.0.1:8787/api
|
|
777
|
+
```
|
|
778
|
+
|
|
779
|
+
A route is `Request` in and `Response` out, the shapes every runtime speaks,
|
|
780
|
+
and `packages/dock/harbor/http.ts` is that contract. A handler answering
|
|
781
|
+
nothing declines and the mount tries what comes after it. `listenHttp` is
|
|
782
|
+
the one place Node's own objects appear: its door turns an `IncomingMessage`
|
|
783
|
+
into a `Request` and writes a `Response` back onto a `ServerResponse`, and
|
|
784
|
+
no route names either. So the same route runs on this daemon, on a worker at
|
|
785
|
+
the edge, in a browser's own service worker, on Deno and on Bun, and the
|
|
786
|
+
terrain is the door and never the side.
|
|
787
|
+
|
|
788
|
+
The daemon listens on loopback only, and only when asked, with `--http PORT`
|
|
789
|
+
or `QUO_HTTP`. The proxy faces the world and terminates TLS; the daemon never
|
|
790
|
+
does. `quo estate` writes a systemd unit that runs the daemon as one user
|
|
791
|
+
forever, a launchd agent that does the same on a Mac, and a Caddyfile that
|
|
792
|
+
maps the hostnames onto the one port by path, from the templates in
|
|
793
|
+
`packages/dock/cli/estate/`.
|
|
794
|
+
Unattended, the daemon is that unit: restarted if it dies, the lease
|
|
795
|
+
released on SIGTERM, and every ward rebooted from its folder on the next
|
|
796
|
+
start with relations intact. A Mac is a dialer and not a listener: its
|
|
797
|
+
agent opens no HTTP door, and the routes it holds a socket to are rows the
|
|
798
|
+
root kept on its `dialers` faculty.
|
|
799
|
+
|
|
800
|
+
The socket is the root owner. Owning a droplet from elsewhere is a standing
|
|
801
|
+
at its ward, minted by the root, carried as a sealed ask like any relation.
|
|
802
|
+
There is no `cli.` route.
|
|
803
|
+
|
|
804
|
+
**Where the addresses come from, per terrain.** One convention holds
|
|
805
|
+
everywhere: the four routes are `mcp.`, `web.`, `quo.` and `api.` under one
|
|
806
|
+
domain, over https, and nobody types a hostname four times. On a droplet
|
|
807
|
+
the domain is said once, `quo init --domain acme.com` or `quo domain
|
|
808
|
+
acme.com`, and `<dir>/routes.json` is written by the convention; an estate
|
|
809
|
+
whose names break it edits that file, and it is the one file that stands
|
|
810
|
+
beside the wards by right, since a ward knows its pk and never its
|
|
811
|
+
hostname, which is the proxy's fact. With no `routes.json` the daemon
|
|
812
|
+
serves `/web`, `/api` and `/quo` on its loopback door, names that door in
|
|
813
|
+
pages and links, and opens no model door, since OAuth needs a public
|
|
814
|
+
issuer. The app types nothing: it opens a link, the link carries the
|
|
815
|
+
world's `quo.` hint, and the world's `GET /web/` hands it the harbor's
|
|
816
|
+
whole config. The edge is its own origin, `<origin>/h/<name>/quo`, known at
|
|
817
|
+
deploy. A tab is the origin it was served from, with the `quo` address in
|
|
818
|
+
the page's config script. So a domain is said in one place, on the box,
|
|
819
|
+
and every other terrain learns the addresses from a link or a request.
|
|
820
|
+
|
|
821
|
+
## Acceptance
|
|
822
|
+
|
|
823
|
+
A harbor here is accepted when it passes the library's conformance suite
|
|
824
|
+
untouched, and by nothing else. Three do: the disk harbor over the wire
|
|
825
|
+
under both reaches, the browser harbor through a daemon, and the edge harbor
|
|
826
|
+
inside the platform's runtime. Every store passes the library's store suite,
|
|
827
|
+
`packages/quo/src/conformance/store.ts`: files in
|
|
828
|
+
`packages/dock/test/store.test.ts`, IndexedDB and the edge's storage inside
|
|
829
|
+
their terrains behind `deep:dock`. Every reach passes the library's reach
|
|
830
|
+
suite, `packages/quo/src/conformance/reach.ts`. The store, the reach and the
|
|
831
|
+
core are the library's, and every harbor here is one of them over its own
|
|
832
|
+
terrain.
|
|
833
|
+
|
|
834
|
+
## Where the tree stands
|
|
835
|
+
|
|
836
|
+
The one place under this paper where a gap between it and the folders is
|
|
837
|
+
named. Each line is a debt to close, not a note to keep.
|
|
838
|
+
|
|
839
|
+
- **A torn write is read, not refused.** A plain store keeps a ward as
|
|
840
|
+
rows written one after another, and a write interrupted between two of
|
|
841
|
+
them leaves rows that do not belong to one another. The partition
|
|
842
|
+
assembled from them boots; it should refuse to, and say which row is
|
|
843
|
+
wrong, the way a shape it cannot read already does.
|
|
844
|
+
- **A sealed store is one value per ward, and whether it should be rows is
|
|
845
|
+
undecided.** The phone and the desk seal the whole ward under one key, so
|
|
846
|
+
a being with much in her cells costs her neighbours there as she costs
|
|
847
|
+
nobody on a droplet, in a tab or on the edge. Rows there are one seal per
|
|
848
|
+
being and move the custody rule the sealed store is written around, and
|
|
849
|
+
that is a question about what a restore means before it is one about
|
|
850
|
+
speed. The human decides it, and not in passing.
|