@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.
- package/LICENSE +202 -0
- package/NOTICE +6 -0
- package/README.md +27 -0
- package/beings/avatar.ts +84 -0
- package/beings/desk.ts +46 -0
- package/beings/index.ts +5 -0
- package/beings/quo-dock.md +698 -0
- package/beings/setup.ts +23 -0
- package/beings/side.ts +51 -0
- package/beings/user.ts +78 -0
- package/cli/client.ts +65 -0
- package/cli/daemon.ts +582 -0
- package/cli/quo.ts +126 -0
- package/dist/beings/avatar.d.ts +32 -0
- package/dist/beings/avatar.js +82 -0
- package/dist/beings/desk.d.ts +42 -0
- package/dist/beings/desk.js +43 -0
- package/dist/beings/index.d.ts +3 -0
- package/dist/beings/index.js +5 -0
- package/dist/beings/setup.d.ts +2 -0
- package/dist/beings/setup.js +16 -0
- package/dist/beings/side.d.ts +25 -0
- package/dist/beings/side.js +37 -0
- package/dist/beings/user.d.ts +108 -0
- package/dist/beings/user.js +74 -0
- package/dist/cli/client.d.ts +14 -0
- package/dist/cli/client.js +51 -0
- package/dist/cli/daemon.d.ts +57 -0
- package/dist/cli/daemon.js +581 -0
- package/dist/cli/quo.d.ts +2 -0
- package/dist/cli/quo.js +121 -0
- package/dist/harbor/browser.d.ts +13 -0
- package/dist/harbor/browser.js +50 -0
- package/dist/harbor/disk.d.ts +19 -0
- package/dist/harbor/disk.js +126 -0
- package/dist/harbor/edge/edge.d.ts +26 -0
- package/dist/harbor/edge/edge.js +123 -0
- package/dist/harbor/edge/exercise.d.ts +7 -0
- package/dist/harbor/edge/exercise.js +179 -0
- package/dist/harbor/edge/storage.d.ts +20 -0
- package/dist/harbor/edge/storage.js +81 -0
- package/dist/harbor/edge/worker.d.ts +14 -0
- package/dist/harbor/edge/worker.js +47 -0
- package/dist/harbor/files.d.ts +14 -0
- package/dist/harbor/files.js +83 -0
- package/dist/harbor/idb.d.ts +15 -0
- package/dist/harbor/idb.js +85 -0
- package/dist/human/dom.d.ts +2 -0
- package/dist/human/dom.js +15 -0
- package/dist/human/html.d.ts +32 -0
- package/dist/human/html.js +136 -0
- package/dist/human/screen.d.ts +11 -0
- package/dist/human/screen.js +66 -0
- package/dist/human/tab.d.ts +7 -0
- package/dist/human/tab.js +68 -0
- package/dist/mcp/agent.d.ts +10 -0
- package/dist/mcp/agent.js +78 -0
- package/dist/mcp/http.d.ts +27 -0
- package/dist/mcp/http.js +96 -0
- package/dist/mcp/oauth.d.ts +98 -0
- package/dist/mcp/oauth.js +241 -0
- package/dist/mcp/pilot.d.ts +6 -0
- package/dist/mcp/pilot.js +68 -0
- package/dist/mcp/runner.d.ts +45 -0
- package/dist/mcp/runner.js +94 -0
- package/dist/mcp/server.d.ts +9 -0
- package/dist/mcp/server.js +63 -0
- package/dist/mcp/web/exchange.d.ts +23 -0
- package/dist/mcp/web/exchange.js +136 -0
- package/harbor/browser.ts +63 -0
- package/harbor/disk.ts +120 -0
- package/harbor/edge/edge.ts +147 -0
- package/harbor/edge/exercise.ts +187 -0
- package/harbor/edge/platform.d.ts +28 -0
- package/harbor/edge/storage.ts +82 -0
- package/harbor/edge/worker.ts +47 -0
- package/harbor/edge/wrangler.toml +21 -0
- package/harbor/files.ts +89 -0
- package/harbor/idb.ts +90 -0
- package/harbor/quo-harbor.md +403 -0
- package/human/dom.ts +21 -0
- package/human/html.ts +164 -0
- package/human/quo-human.md +86 -0
- package/human/screen.ts +76 -0
- package/human/tab.ts +84 -0
- package/mcp/agent.ts +94 -0
- package/mcp/http.ts +96 -0
- package/mcp/oauth.ts +260 -0
- package/mcp/pilot.ts +73 -0
- package/mcp/quo-mcp.md +262 -0
- package/mcp/runner.ts +131 -0
- package/mcp/server.ts +67 -0
- package/mcp/web/exchange.ts +146 -0
- 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
|
+
}
|