@quo-systems/dock 0.1.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.
Files changed (94) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +6 -0
  3. package/README.md +27 -0
  4. package/beings/avatar.ts +84 -0
  5. package/beings/desk.ts +46 -0
  6. package/beings/index.ts +5 -0
  7. package/beings/quo-dock.md +698 -0
  8. package/beings/setup.ts +23 -0
  9. package/beings/side.ts +51 -0
  10. package/beings/user.ts +78 -0
  11. package/cli/client.ts +65 -0
  12. package/cli/daemon.ts +582 -0
  13. package/cli/quo.ts +126 -0
  14. package/dist/beings/avatar.d.ts +32 -0
  15. package/dist/beings/avatar.js +82 -0
  16. package/dist/beings/desk.d.ts +42 -0
  17. package/dist/beings/desk.js +43 -0
  18. package/dist/beings/index.d.ts +3 -0
  19. package/dist/beings/index.js +5 -0
  20. package/dist/beings/setup.d.ts +2 -0
  21. package/dist/beings/setup.js +16 -0
  22. package/dist/beings/side.d.ts +25 -0
  23. package/dist/beings/side.js +37 -0
  24. package/dist/beings/user.d.ts +108 -0
  25. package/dist/beings/user.js +74 -0
  26. package/dist/cli/client.d.ts +14 -0
  27. package/dist/cli/client.js +51 -0
  28. package/dist/cli/daemon.d.ts +57 -0
  29. package/dist/cli/daemon.js +581 -0
  30. package/dist/cli/quo.d.ts +2 -0
  31. package/dist/cli/quo.js +121 -0
  32. package/dist/harbor/browser.d.ts +13 -0
  33. package/dist/harbor/browser.js +50 -0
  34. package/dist/harbor/disk.d.ts +19 -0
  35. package/dist/harbor/disk.js +126 -0
  36. package/dist/harbor/edge/edge.d.ts +26 -0
  37. package/dist/harbor/edge/edge.js +123 -0
  38. package/dist/harbor/edge/exercise.d.ts +7 -0
  39. package/dist/harbor/edge/exercise.js +179 -0
  40. package/dist/harbor/edge/storage.d.ts +20 -0
  41. package/dist/harbor/edge/storage.js +81 -0
  42. package/dist/harbor/edge/worker.d.ts +14 -0
  43. package/dist/harbor/edge/worker.js +47 -0
  44. package/dist/harbor/files.d.ts +14 -0
  45. package/dist/harbor/files.js +83 -0
  46. package/dist/harbor/idb.d.ts +15 -0
  47. package/dist/harbor/idb.js +85 -0
  48. package/dist/human/dom.d.ts +2 -0
  49. package/dist/human/dom.js +15 -0
  50. package/dist/human/html.d.ts +32 -0
  51. package/dist/human/html.js +136 -0
  52. package/dist/human/screen.d.ts +11 -0
  53. package/dist/human/screen.js +66 -0
  54. package/dist/human/tab.d.ts +7 -0
  55. package/dist/human/tab.js +68 -0
  56. package/dist/mcp/agent.d.ts +10 -0
  57. package/dist/mcp/agent.js +78 -0
  58. package/dist/mcp/http.d.ts +27 -0
  59. package/dist/mcp/http.js +96 -0
  60. package/dist/mcp/oauth.d.ts +98 -0
  61. package/dist/mcp/oauth.js +241 -0
  62. package/dist/mcp/pilot.d.ts +6 -0
  63. package/dist/mcp/pilot.js +68 -0
  64. package/dist/mcp/runner.d.ts +45 -0
  65. package/dist/mcp/runner.js +94 -0
  66. package/dist/mcp/server.d.ts +9 -0
  67. package/dist/mcp/server.js +63 -0
  68. package/dist/mcp/web/exchange.d.ts +23 -0
  69. package/dist/mcp/web/exchange.js +136 -0
  70. package/harbor/browser.ts +63 -0
  71. package/harbor/disk.ts +120 -0
  72. package/harbor/edge/edge.ts +147 -0
  73. package/harbor/edge/exercise.ts +187 -0
  74. package/harbor/edge/platform.d.ts +28 -0
  75. package/harbor/edge/storage.ts +82 -0
  76. package/harbor/edge/worker.ts +47 -0
  77. package/harbor/edge/wrangler.toml +21 -0
  78. package/harbor/files.ts +89 -0
  79. package/harbor/idb.ts +90 -0
  80. package/harbor/quo-harbor.md +403 -0
  81. package/human/dom.ts +21 -0
  82. package/human/html.ts +164 -0
  83. package/human/quo-human.md +86 -0
  84. package/human/screen.ts +76 -0
  85. package/human/tab.ts +84 -0
  86. package/mcp/agent.ts +94 -0
  87. package/mcp/http.ts +96 -0
  88. package/mcp/oauth.ts +260 -0
  89. package/mcp/pilot.ts +73 -0
  90. package/mcp/quo-mcp.md +262 -0
  91. package/mcp/runner.ts +131 -0
  92. package/mcp/server.ts +67 -0
  93. package/mcp/web/exchange.ts +146 -0
  94. package/package.json +89 -0
package/harbor/idb.ts ADDED
@@ -0,0 +1,90 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // The store as IndexedDB, per origin: one database per harbor, one record
3
+ // per ward holding seed, partition and record, and one record for the
4
+ // directory's hints. The honest weakness the harbor document names: a tab
5
+ // has no secret store for bytes, so the seed sits beside the partition,
6
+ // guarded by the origin and nothing more. That is the price of a tab.
7
+ import { values, type Kept, type Store, type WardRecord } from '@quo-systems/quo/harbor';
8
+
9
+ const WARDS = 'wards',
10
+ HINTS = 'hints';
11
+
12
+ function open(name: string): Promise<IDBDatabase> {
13
+ return new Promise((ok, no) => {
14
+ const req = indexedDB.open(name, 1);
15
+ req.onupgradeneeded = () => {
16
+ req.result.createObjectStore(WARDS);
17
+ req.result.createObjectStore(HINTS);
18
+ };
19
+ req.onsuccess = () => ok(req.result);
20
+ req.onerror = () => no(req.error ?? new Error('indexedDB would not open'));
21
+ });
22
+ }
23
+
24
+ function done<T>(req: IDBRequest<T>): Promise<T> {
25
+ return new Promise((ok, no) => {
26
+ req.onsuccess = () => ok(req.result);
27
+ req.onerror = () => no(req.error ?? new Error('indexedDB request failed'));
28
+ });
29
+ }
30
+
31
+ type Row = { seed: Uint8Array; partition: Record<string, unknown>; record: WardRecord };
32
+
33
+ export class Idb implements Store {
34
+ readonly name: string;
35
+ #db: Promise<IDBDatabase> | undefined;
36
+ constructor(name = 'quo') {
37
+ this.name = name;
38
+ }
39
+ #open(): Promise<IDBDatabase> {
40
+ return (this.#db ??= open(this.name));
41
+ }
42
+ async #tx(store: string, mode: IDBTransactionMode): Promise<IDBObjectStore> {
43
+ return (await this.#open()).transaction(store, mode).objectStore(store);
44
+ }
45
+
46
+ async list(): Promise<string[]> {
47
+ return (await done((await this.#tx(WARDS, 'readonly')).getAllKeys())).map(String);
48
+ }
49
+ async load(name: string): Promise<Kept | undefined> {
50
+ const row = (await done((await this.#tx(WARDS, 'readonly')).get(name))) as Row | undefined;
51
+ return row && { seed: new Uint8Array(row.seed), partition: values(row.partition), record: row.record };
52
+ }
53
+ async put(name: string, kept: Kept): Promise<void> {
54
+ const st = await this.#tx(WARDS, 'readwrite');
55
+ if ((await done(st.getKey(name))) !== undefined) throw new Error(`ward ${name} already exists in ${this.name}`);
56
+ await done(st.put({ seed: kept.seed, partition: values(kept.partition), record: kept.record } satisfies Row, name));
57
+ }
58
+ async save(name: string, partition: Record<string, unknown>): Promise<void> {
59
+ const st = await this.#tx(WARDS, 'readwrite');
60
+ const row = (await done(st.get(name))) as Row | undefined;
61
+ if (!row) return;
62
+ await done(st.put({ ...row, partition: values(partition) } satisfies Row, name));
63
+ }
64
+ async record(name: string, record: WardRecord): Promise<void> {
65
+ const st = await this.#tx(WARDS, 'readwrite');
66
+ const row = (await done(st.get(name))) as Row | undefined;
67
+ if (!row) return;
68
+ await done(st.put({ ...row, record } satisfies Row, name));
69
+ }
70
+ async take(name: string): Promise<Kept | undefined> {
71
+ const kept = await this.load(name);
72
+ if (!kept) return undefined;
73
+ await done((await this.#tx(WARDS, 'readwrite')).delete(name));
74
+ return kept;
75
+ }
76
+ async hints(): Promise<Record<string, string>> {
77
+ const st = await this.#tx(HINTS, 'readonly');
78
+ const keys = (await done(st.getAllKeys())).map(String);
79
+ const vals = (await done(st.getAll())) as string[];
80
+ return Object.fromEntries(keys.map((k, i) => [k, vals[i]!]));
81
+ }
82
+ async hint(pk: string, url: string): Promise<void> {
83
+ await done((await this.#tx(HINTS, 'readwrite')).put(url, pk));
84
+ }
85
+
86
+ close(): void {
87
+ void this.#db?.then((db) => db.close());
88
+ this.#db = undefined;
89
+ }
90
+ }
@@ -0,0 +1,403 @@
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 `packages/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
+ ## Three parts, three native places
34
+
35
+ The harbor adapts nothing. Every terrain already has a place for secrets, a
36
+ place for data and a place for code, and those are the three the harbor
37
+ needs:
38
+
39
+ | a ward is | Mac, iPhone | droplet | edge | browser tab |
40
+ | ----------- | --------------------- | -------------------------- | ----------------------- | ---------------------- |
41
+ | seed | Keychain | `.env` or a secrets file | the platform's secrets | IndexedDB, per origin |
42
+ | partition | app data, SQLite | a file on disk | Durable Object storage | IndexedDB, per origin |
43
+ | code | the app bundle | a folder | the deployed worker | a bundle URL |
44
+
45
+ Envoys read their own secrets from the same secret store as the seed. A
46
+ device has one place for secrets and the harbor is one more user of it.
47
+
48
+ The browser column is the honest weakness: a tab has no secret store for
49
+ bytes, so the seed sits beside the partition, guarded by the origin and
50
+ nothing more. That is the price of a tab, and why a tab is a rented room and
51
+ a native app is not.
52
+
53
+ ### The partition, split or wrapped
54
+
55
+ Inside the partition one branch is secret, the bind table: each standing's
56
+ current and next keys, and the heirs minted and not yet spoken. Everything
57
+ else is data. The partition is values only and the harbor may persist it as
58
+ it likes, so it may keep the secret branch apart from the data, by terrain:
59
+
60
+ - **Split.** The bind branch goes to the secret store beside the seed, the
61
+ rest to the data store. Fits a phone with a few relations; keychain items
62
+ are small and a being with a thousand standings has a thousand key pairs.
63
+ - **Wrapped.** The secret store holds one key per ward; the data store holds
64
+ the whole partition encrypted under it. Any size, one secret. The droplet
65
+ and edge answer, and it works on the phone too.
66
+
67
+ The ward never knows. It hands values and gets values back. A heir given away
68
+ as an invitation is a live credential until it speaks, and on a device other
69
+ apps share that is the argument for the secret store.
70
+
71
+ ### Custody is a lease
72
+
73
+ `packages/quo/SPEC.md` makes custody the harbor's vouch: two harbors over one
74
+ seed are two wards with one pk, and a harbor refuses to boot a ward another
75
+ running harbor holds. The dock makes that a property of the store, not a rule
76
+ people follow: a lease beside the seed in the secret store, taken by the daemon
77
+ that boots the ward, checked before any boot, refused while held. A restored
78
+ backup meets a held lease and does not boot. A second tab on one origin meets a
79
+ lock and becomes a screen. The edge is a Durable Object, which is the lease
80
+ built in. None of it stops a deliberate copy, and nothing can; it stops every
81
+ accident, and accidents are the whole population of this problem.
82
+
83
+ ### Migration is three moves
84
+
85
+ A ward moves between harbors by moving its three parts: the seed from one
86
+ secret store to another, the partition from one data store to another, and
87
+ the new harbor pointed at the same code. Same seed, same pk, every standing
88
+ anyone holds still points at her. The old harbor deletes its copies first:
89
+ two harbors holding one seed are two wards with one pk, diverging silently.
90
+
91
+ ### Code is a harbor decision
92
+
93
+ A ward's code has an origin: a folder the owner wrote, a bundle a world
94
+ published, a worker deployed. When the origin ships new bodies the harbor
95
+ decides whether the ward reboots on them. A reboot is silent, the ward cannot
96
+ tell old bodies from new, and a body the cells were not written for is an
97
+ absence this run. So the harbor pins what a ward boots on, records it in the
98
+ ward record, and moves it deliberately: on its own for the owner's own code,
99
+ with the owner's word for a world's.
100
+
101
+ ## The daemon, and the two cores
102
+
103
+ There are two harbor cores, and every device runs one of them:
104
+
105
+ - **The node daemon**, `quo serve`. A disk or keychain store, a unix socket
106
+ for the owner, listeners on localhost for the routes. Droplet, Mac, Linux
107
+ box, and a desktop app that ships it as a sidecar.
108
+ - **The browser harbor.** A dialer inside a webview or a tab, an IndexedDB or
109
+ SQLite store, one socket per world it is connected to. A tab, and a mobile
110
+ app that wraps one.
111
+
112
+ Tauri and Capacitor are packaging, not harbors. A desktop app ships the
113
+ daemon and a window, and is owner-capable through the socket. A mobile app
114
+ ships the browser harbor with a native store and push, and is its own owner
115
+ for its one harbor. The edge is the daemon's core over a Durable Object, one
116
+ single-writer object per ward, or it diverges.
117
+
118
+ A wrapped app carries its own bundle and never loads it from a hostname.
119
+ Only a plain tab fetches the bundle from a world's `web.`, because it has
120
+ nowhere else to get it. A wrapped app visits a world's `web.` exactly once,
121
+ in a system browser sheet, to do the credential exchange and come back with
122
+ an invitation; after that it speaks to that world's `quo.` for bytes and
123
+ never to its `web.` again. One app on a device meets many worlds, one ward
124
+ each, and there is never an app per world.
125
+
126
+ ## Listeners and dialers
127
+
128
+ Two kinds of harbor exist in the world, and every pair reduces to them:
129
+
130
+ | terrain | can be dialed | can dial | lifetime | wakes on |
131
+ | ------------ | ------------- | -------- | ----------------------- | -------- |
132
+ | droplet | yes | yes | as long as wanted | itself |
133
+ | edge | yes | fetch | per request | request |
134
+ | desktop app | no | yes | while running | itself |
135
+ | phone app | no | yes | foreground, throttled | push |
136
+ | browser tab | no | yes | while open | itself |
137
+
138
+ A ward on a dialer is reached only if its harbor dialed first and something
139
+ holds that line open. A phone's ask must survive a push wake-up. A tab is a
140
+ device, and one seed never lives in two tabs.
141
+
142
+ ## Transport by necessity
143
+
144
+ Transport is chosen by necessity and nothing else. Two wards in one harbor:
145
+ no transport, the harbor calls its other door. Both harbors can listen: a
146
+ request, plain HTTP or raw TCP, and a reverse ask is a request the other
147
+ way. One of them cannot listen, behind NAT, in a tab, on a phone: a socket,
148
+ opened by the one that can dial and held by the one that can listen, with
149
+ both sides sending asks over it and a frame id matching replies. There is no
150
+ fourth situation, so there is no fourth reach, and none is ever picked
151
+ because it is nice. A being cannot tell which one carried her ask, and that
152
+ is the proof the choice was free.
153
+
154
+ A connection is a reach, never a harbor, and a ward outlives it. When a
155
+ socket drops, the listener's directory entry for that pk goes with it, the
156
+ listener answers nothing, the dialer's harbor turns that into unreached, and
157
+ the relation is untouched: seed, partition and keys are on the dialer, not on
158
+ the wire. When the dialer reconnects it announces its pks, the listener
159
+ probes each one at its door and rebinds what proved, and the next ask goes
160
+ through under the same keys and the same count. A phone in a tunnel loses
161
+ signal, never identity.
162
+
163
+ Untouched is the relation and not her record of it. An ask that reached
164
+ nobody has still taken its number and staged the key it announced, because
165
+ she must never offer one number twice; the count carries the gap and the far
166
+ door honours any number above its mark. So a tunnel costs a number and a
167
+ rotation, and nothing else.
168
+
169
+ ## Reach, directory, rendezvous
170
+
171
+ A harbor needs three reach kinds and nothing more:
172
+
173
+ - **request**: a URL. Send bytes, get bytes. Listener to listener.
174
+ - **socket**: a held connection, opened by whichever side can dial, used in
175
+ both directions with a frame id. A WebSocket, a WebRTC channel and a
176
+ native socket are one socket reach.
177
+ - **channel**: an in-process port between two runtimes on one device. Tabs
178
+ on one machine, a webview and its sidecar.
179
+
180
+ Plus one forwarding rule: a pk whose reach is another harbor that holds a
181
+ socket to it. That rule is the **rendezvous**, and with it every dialer is
182
+ reachable: a phone holds one socket to `quo.acme.com`, acme's harbor binds
183
+ the phone's pk to that socket, and a third harbor sending to the phone's pk
184
+ sends to acme, which forwards the same bytes to the same pk. Nobody changes
185
+ a pk or a byte, and an intermediary opens nothing.
186
+
187
+ What it does learn is worth writing down, because "pks and ciphertext" is
188
+ true and reads as a smaller thing than it is. A harbor forwarding for
189
+ someone else holds, for every ask it carries: the destination ward pk, in
190
+ the clear, because that is the address and there is no forwarding without
191
+ it; the socket it came in on, and so the dialer's own address; the size of
192
+ the ask and of the reply, which are not padded and so move with the
193
+ plaintext; the time each crossed, and which reply belongs to which ask,
194
+ because it awaits the one to answer the other; and, from the announce
195
+ frame, which ward pks are held by one device. So an operator of a
196
+ rendezvous can keep who spoke to whom, when, and how much, for as long as
197
+ it likes, and break no ciphertext to do it. What it cannot learn is the
198
+ relation: the heir is sealed inside the ask, so one standing at a ward
199
+ cannot be told from another, nor followed from one rendezvous to the next.
200
+ Being reachable through someone costs the fact that you spoke. It does not
201
+ cost what you said, nor who you are to the far being.
202
+
203
+ A rendezvous is a listener and nothing more, so there is never one of them.
204
+ A harbor may hold sockets to several and be reached through any: the
205
+ directory is keyed by pk and a bind is per socket, so each listener binds
206
+ the same pks independently. Plurality is the design and not a deployment
207
+ habit, and a world that can only be reached through one listener chose
208
+ that.
209
+
210
+ A line also says which **wire suite** it speaks, one number that is not
211
+ negotiated: in the text frame a socket announces itself with, and as the
212
+ header `quo-suite` on a request. It is never on an ask, so an ask is bytes
213
+ with nothing in front of them, and a harbor still knows before it carries
214
+ anything whether the far side can open what it would send. A side meeting a
215
+ suite it does not know closes the line; a request door answers nothing
216
+ delivered. `packages/quo/SPEC.md` owns the number and says why it exists.
217
+
218
+ The **directory** is the map from pk to reach. The memory harbor stubs it with
219
+ linked peers. A real one is a table the harbor fills however it likes, and the
220
+ one entry it must learn by itself is a dialer's: when a dialer connects it
221
+ announces the ward pks it holds, and the listener binds to that socket each one
222
+ whose door answered its probe; `packages/quo/SPEC.md` says what the probe is.
223
+ After a migration the pk is unchanged, only its reach, and the next connect
224
+ rebinds it.
225
+
226
+ ## The quo. route, as built
227
+
228
+ The reach interface and its two implementations are the library's,
229
+ `packages/quo/src/harbor/reach.ts`, and pass its reach suite here: the request
230
+ and a socket to a daemon in `packages/dock/test/wire.test.ts`, the request
231
+ through the worker inside the edge. The listener half is the terrain's, below:
232
+
233
+ ```
234
+ request POST <url>/<pk>, the sealed ask as the body, the sealed reply as the answer;
235
+ <url> is the world's quo. route, which the proxy maps onto the daemon's /quo
236
+ 404 is "no reach for that pk" and comes back as nothing; a line lost after
237
+ sending answers nothing at all, and the ward's bound ends the ask
238
+ socket a WebSocket at <url>, held by the dialer, used both ways
239
+ text frame { announce: [pk, ...] } the ward pks this side holds
240
+ ask frame [0][id 4][pk 64][bytes] a frame id matches the reply
241
+ reply frame [1][id 4][bytes]
242
+ none frame [2][id 4] nothing was delivered
243
+ directory pk -> reach, filled three ways: a socket a dialer holds, bound once proven
244
+ and unbound at close; a hint from a link, `quo reach <pk> <url>`, kept in
245
+ <dir>/reach.json; and the harbor's own doors, always first
246
+ ```
247
+
248
+ One rule makes the rendezvous: bytes that arrive from the wire go to an own
249
+ door or to a socket this harbor holds for that pk, and never onward by
250
+ request. So a third harbor with a hint that a phone's pk is at acme's
251
+ `quo.` sends there, and acme forwards down the socket the phone holds.
252
+ `quo serve --dial URL`, or `<dir>/dial.json`, makes a daemon a dialer: it
253
+ holds one socket per URL, announces its wards on open, binds the listener's
254
+ pks, and reconnects with backoff from a second to thirty when the line
255
+ drops. A pk is 64 bytes on the wire, the ward key and the padlock, which the
256
+ first version of the ask frame got wrong and the suite caught in one run.
257
+
258
+ The proof is `packages/dock/test/wire.test.ts`: the library's conformance suite,
259
+ untouched, against two disk harbors in two folders with HTTP doors on
260
+ loopback, once with every ask a request and once with the second harbor
261
+ dialing the first; down is the reach refused, migrate is the ward folder
262
+ moved and adopted. Then the rendezvous on its own. What it taught: a harbor
263
+ must write the partition before it lets a folder go, because a being driven
264
+ in-process changes it without passing a door; listeners left open keep a
265
+ test process alive forever, so every pair is closed after the file; and a
266
+ proxy sends the route root as `/quo/`, trailing slash, which the door takes.
267
+ The first live crossing, Mac to lab and back, is in `estates/lab/lab.md`.
268
+
269
+ ## The two cores, as built
270
+
271
+ The harbor core is the library's, `packages/quo/src/harbor/core.ts`, and so is
272
+ the dialer, `packages/quo/src/harbor/dial.ts`; `packages/quo/SPEC.md` says what
273
+ they are. Every harbor here extends the core and hands it three things: a
274
+ **store**, the library's interface, with `files.ts` on a disk and `idb.ts` in a
275
+ tab, each passing the library's store suite; a **loader**, the code half, a
276
+ module from a folder on a daemon and the bundle in a tab; and a **lease**, a pid
277
+ file on disk and a web lock on the database name in a tab, so a second tab on
278
+ one origin meets the lock and is a screen. The disk harbor, `disk.ts`, is the
279
+ core plus files, the loader and the pid, and holds the `ws` listener's end of
280
+ every socket dialed to it; the browser harbor, `browser.ts`, is the core plus
281
+ IndexedDB, the built-in beings, the lock and one dialer per world.
282
+
283
+ A tab's store keeps values through JSON, as a file does, because the ward
284
+ hands its partition out through a guard that structured clone refuses.
285
+ The daemon's `/quo` and `/health` answer any origin, with the preflight a
286
+ binary POST needs, because a tab on one world's `web.` reaching another
287
+ world's `quo.` is the ordinary case.
288
+
289
+ `/health` asks for nothing and tells what this harbor hosts: the wards by
290
+ name and pk, the sockets held, and the directory. That is a map of the
291
+ device, so it is an operator's surface and not a route: the proxy maps the
292
+ three hostnames onto their paths and nothing onto this one, and a request
293
+ for it from outside meets a 404 at the proxy. It answers any origin because
294
+ it is on loopback, and the day it is put behind a proxy it needs a reason
295
+ and a door.
296
+
297
+ The proof is `packages/dock/test/terrain/browser.test.ts`, behind
298
+ `npm run check:terrain` beside the library's own browser chapter: the
299
+ conformance suite, untouched, inside a real Chromium, against two browser
300
+ harbors in one tab with two databases, both dialing a Node daemon on
301
+ loopback, so every relation crosses the rendezvous; down is the reach
302
+ refused on both, migrate lifts seed, partition and record from one database
303
+ into the other and boots there, the tab-to-home scene. And the lock: a
304
+ second harbor over one database does not boot. It passed on 2026-09-04.
305
+
306
+ ## The edge, as built
307
+
308
+ `packages/dock/harbor/edge/` is the core over one Durable Object, and the object
309
+ is the harbor. The platform runs one instance of it at a time, which is
310
+ the lease and the single writer every ward needs. Its storage is the
311
+ store, `storage.ts`: one row per ward, seed, partition and record, with the
312
+ seed sealed under a key from the platform's secrets, `QUO_SEED_KEY`, so the
313
+ storage holds ciphertext and the secret store holds the one key, as the
314
+ table above says. The deployed worker is the code, the built-in beings and
315
+ whatever it hands in. It is a listener and never a dialer: reached by
316
+ request at `<origin>/h/<name>/quo`, holding the sockets dialers open to it
317
+ on the platform's own socket pair, the rendezvous for them, awake per
318
+ request and kept awake by a held socket.
319
+
320
+ Its owner door is a route, not a socket, because the platform has no local
321
+ process: the root is whoever holds `QUO_ROOT`, a platform secret, and the
322
+ root's asks arrive as one POST at `<origin>/h/<name>/root` under it, the
323
+ first of them `{ init: { ward, user } }`, the root's setup. That is the
324
+ device's own rule on this terrain, as file permissions are on a disk. The
325
+ first thing a root on the edge does is invite an owner on the ward's pk,
326
+ and from then on the edge is piloted from elsewhere over a standing, with
327
+ `--via`, like any ward.
328
+
329
+ The proof is `packages/dock/test/terrain/edge.test.ts`, behind
330
+ `npm run check:terrain`, in the platform's own runtime on loopback under
331
+ `wrangler dev`: the conformance suite, untouched, inside the platform
332
+ against two edge harbors in one object over two prefixed views of its
333
+ storage, reaching each other by request through the worker; the root door
334
+ refused without the secret and answering under it; and a Node daemon
335
+ dialing the edge, its pk held there, reached by a third harbor through the
336
+ edge, unreached when the line drops. What it taught: one object may not
337
+ touch another's storage, the platform says so at the first put, and so the
338
+ two harbors of the exercise share an object the way two tabs share a page;
339
+ and a Uint8Array over a shared buffer is refused by the platform's crypto,
340
+ which wants a plain one.
341
+
342
+ The `ws` question the road asked is answered by this terrain: the socket
343
+ reach's framing and the dialer are written on the standard surface and
344
+ hold on every terrain; the listener half is the terrain's own, `ws` on
345
+ Node and the socket pair on the edge, and stays where the terrain is. What
346
+ the library takes, when it takes the reach, is the framing and the dialer.
347
+
348
+ ## The link
349
+
350
+ An invitation carries the ward pk of the world that minted it, and not where
351
+ that pk lives. A harbor that has never heard of that world cannot carry the
352
+ first knock. So a link is an invitation next to a **reach hint**, the URL of
353
+ that world's `quo.` route. The being receives the invitation opaque, as
354
+ always; the hint goes to the directory. The link is single-use, because the
355
+ heir dies at the first knock, and whoever opens it first is bound.
356
+
357
+ ## Routes on a daemon
358
+
359
+ ```
360
+ device
361
+ quo serve one process, one harbor, the ask pointer
362
+ quo.sock owner asks; local only; never behind a proxy
363
+ side.sock local sides; local only; never behind a proxy
364
+ 127.0.0.1:8787 the HTTP door, loopback only; routes mount by path
365
+ /health the wards it hosts, by name and pk
366
+ /mcp the model side
367
+ /web the bundle for tabs, and the exchange pages
368
+ /quo later: the socket door: request in, sockets held, the rendezvous
369
+ reverse proxy
370
+ mcp.example.com -> 127.0.0.1:8787/mcp
371
+ web.example.com -> 127.0.0.1:8787/web
372
+ quo.example.com -> 127.0.0.1:8787/quo
373
+ ```
374
+
375
+ The daemon listens on loopback only, and only when asked, with `--http PORT`
376
+ or `QUO_HTTP`. The proxy faces the world and terminates TLS; the daemon never
377
+ does. `estates/lab/droplet/` holds a systemd unit that runs the daemon as
378
+ one user forever, a launchd agent that does the same on a Mac, and a
379
+ Caddyfile that maps the three hostnames onto the one port by path.
380
+ Unattended, the daemon is that unit: restarted if it dies, the lease
381
+ released on SIGTERM, and every ward rebooted from its folder on the next
382
+ start with relations intact. A Mac is a dialer and not a listener: its
383
+ agent opens no HTTP door, and `~/.quo/dial.json` names the routes it holds
384
+ a socket to.
385
+
386
+ The socket is the root owner. Owning a droplet from elsewhere is a standing
387
+ at its ward, minted by the root, carried as a sealed ask like any relation.
388
+ There is no `cli.` route.
389
+
390
+ ## Acceptance
391
+
392
+ Every harbor here is accepted the day it passes the library's conformance suite
393
+ untouched, and by nothing else. The disk harbor over the wire passed it on
394
+ 2026-09-04, under both reaches; the browser harbor through a daemon passed it
395
+ the same day; the edge harbor inside the platform's runtime passed it the same
396
+ day. Reach, store and the core now hold on three terrains, which is where the
397
+ library takes them. Every store passes the library's store suite,
398
+ `packages/quo/src/conformance/store.ts`: files in
399
+ `packages/dock/test/store.test.ts`, IndexedDB and the edge's storage inside
400
+ their terrains behind `check:terrain`. Every reach passes the library's reach
401
+ suite, `packages/quo/src/conformance/reach.ts`. The store, the reach and the
402
+ core are in the library since 2026-09-04, each with `packages/quo/SPEC.md` in
403
+ the same commit, and every harbor here is one of them over its own terrain.
package/human/dom.ts ADDED
@@ -0,0 +1,21 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // The surface on a document: a page is the root's HTML, and a submit
3
+ // anywhere under it is the form's fields as strings, handed to the side
4
+ // under the ask the form names. The one file under `human/` that touches an
5
+ // element, and it knows nothing of what the page says.
6
+ import type { Surface } from './screen.ts';
7
+ import type { Raw } from './html.ts';
8
+
9
+ export function domSurface(root: HTMLElement): Surface {
10
+ const s: Surface = { show: (html) => (root.innerHTML = html), onSubmit: undefined };
11
+ root.addEventListener('submit', (ev) => {
12
+ const f = ev.target as HTMLFormElement | null;
13
+ const ask = f?.dataset.ask;
14
+ if (!f || ask === undefined) return;
15
+ ev.preventDefault();
16
+ const raw: Raw = {};
17
+ for (const [k, v] of new FormData(f)) raw[k] = typeof v === 'string' ? v : '';
18
+ s.onSubmit?.(ask, raw);
19
+ });
20
+ return s;
21
+ }