@quo-systems/dock 0.1.0 → 0.2.1
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/README.md +9 -2
- package/beings/avatar.ts +6 -2
- package/beings/carry.ts +101 -0
- package/beings/desk.ts +2 -2
- package/beings/index.ts +2 -0
- package/beings/link.ts +54 -0
- package/beings/look.ts +96 -0
- package/beings/quo-dock.md +185 -38
- package/beings/setup.ts +3 -1
- package/beings/side.ts +10 -1
- package/beings/user.ts +31 -5
- package/cli/daemon.ts +98 -44
- package/cli/estate/Caddyfile +25 -0
- package/cli/estate/quo.service +36 -0
- package/cli/estate.ts +44 -0
- package/cli/quo.ts +14 -3
- package/dist/beings/avatar.js +7 -2
- package/dist/beings/carry.d.ts +10 -0
- package/dist/beings/carry.js +106 -0
- package/dist/beings/desk.d.ts +1 -0
- package/dist/beings/desk.js +1 -1
- package/dist/beings/index.d.ts +2 -0
- package/dist/beings/index.js +2 -0
- package/dist/beings/link.d.ts +7 -0
- package/dist/beings/link.js +42 -0
- package/dist/beings/look.d.ts +27 -0
- package/dist/beings/look.js +71 -0
- package/dist/beings/setup.js +4 -1
- package/dist/beings/side.d.ts +8 -1
- package/dist/beings/user.d.ts +26 -2
- package/dist/beings/user.js +32 -5
- package/dist/cli/daemon.d.ts +1 -1
- package/dist/cli/daemon.js +98 -44
- package/dist/cli/estate/Caddyfile +25 -0
- package/dist/cli/estate/quo.service +36 -0
- package/dist/cli/estate.d.ts +5 -0
- package/dist/cli/estate.js +46 -0
- package/dist/cli/quo.js +14 -3
- package/dist/harbor/edge/exercise.js +3 -1
- package/dist/harbor/edge/platform.d.ts +28 -0
- package/dist/human/guest.d.ts +3 -0
- package/dist/human/guest.js +25 -0
- package/dist/human/html.d.ts +10 -2
- package/dist/human/html.js +61 -10
- package/dist/human/screen.d.ts +8 -3
- package/dist/human/screen.js +25 -6
- package/dist/human/tab.d.ts +2 -0
- package/dist/human/tab.js +127 -42
- package/dist/mcp/http.d.ts +4 -3
- package/dist/mcp/http.js +6 -6
- package/dist/mcp/oauth.d.ts +11 -4
- package/dist/mcp/oauth.js +32 -21
- package/dist/mcp/pilot.d.ts +3 -4
- package/dist/mcp/pilot.js +19 -65
- package/dist/mcp/server.d.ts +6 -4
- package/dist/mcp/server.js +48 -11
- package/dist/mcp/web/exchange.d.ts +5 -2
- package/dist/mcp/web/exchange.js +21 -7
- package/harbor/edge/exercise.ts +2 -1
- package/harbor/quo-harbor.md +37 -0
- package/human/guest.ts +26 -0
- package/human/html.ts +58 -10
- package/human/quo-human.md +123 -66
- package/human/screen.ts +28 -7
- package/human/tab.ts +153 -51
- package/mcp/http.ts +10 -9
- package/mcp/oauth.ts +39 -23
- package/mcp/pilot.ts +26 -65
- package/mcp/quo-mcp.md +34 -19
- package/mcp/server.ts +52 -19
- package/mcp/web/exchange.ts +23 -9
- package/package.json +7 -3
package/beings/quo-dock.md
CHANGED
|
@@ -102,7 +102,10 @@ that every arrival is named and judged.
|
|
|
102
102
|
- **Front desk.** A being on the user's harbor that strangers and fresh
|
|
103
103
|
clients meet first. It holds a standing to every user being and can ask
|
|
104
104
|
one for a device invitation. It is the only being that is ever public, and
|
|
105
|
-
only for the first hello.
|
|
105
|
+
only for the first hello. Public is not a kind of being: she is an ordinary
|
|
106
|
+
being who chose to describe herself to strangers and answer them, and what
|
|
107
|
+
she keeps is hers like anyone's. This one keeps little because that is the
|
|
108
|
+
desk's job, not because Quo asks it of her.
|
|
106
109
|
- **Envoy.** A being whose methods call a non-Quo thing and whose describe
|
|
107
110
|
is that thing's surface. A SaaS, an MCP server someone else runs, an
|
|
108
111
|
Arduino behind serial: each enters a world as a being like any other.
|
|
@@ -112,6 +115,12 @@ that every arrival is named and judged.
|
|
|
112
115
|
- **Client identity.** What a credential exchange establishes: this human at
|
|
113
116
|
this browser, this OAuth client for this human, this process on this
|
|
114
117
|
machine. One client identity is one avatar, for life.
|
|
118
|
+
- **Carrier.** A being who shows the asks of the standings she holds as asks
|
|
119
|
+
of her own and forwards. The user being is one: a device the human let
|
|
120
|
+
reach sees acme's asks on her, and acme sees her.
|
|
121
|
+
- **Look.** What a being may say about how she is presented, as values: a
|
|
122
|
+
name, a logo, colours, a font, and a hint per ask. One optional ask,
|
|
123
|
+
`look`, answers it, and every side reads the part it understands.
|
|
115
124
|
- **Route.** A hostname a reverse proxy sends to one process: `web.`, `quo.`,
|
|
116
125
|
`mcp.`. Routes are deployment, never boundaries.
|
|
117
126
|
|
|
@@ -122,7 +131,7 @@ One droplet, one harbor, three routes. Every other placement is a subset.
|
|
|
122
131
|
```
|
|
123
132
|
acme.com, or razvan.com: the dialable part of an estate
|
|
124
133
|
reverse proxy
|
|
125
|
-
web.
|
|
134
|
+
web. one page per world, /<ward>, the credential exchange, ends with an invitation
|
|
126
135
|
quo. the harbor's socket door: the rendezvous for every dialer
|
|
127
136
|
mcp. the model side over HTTP, a credential exchange in front
|
|
128
137
|
quo serve: one process, one harbor, the ask pointer
|
|
@@ -211,6 +220,63 @@ UI and then a tool layer: they build asks, both fall out, and neither side
|
|
|
211
220
|
can do a thing the other cannot, because the gate is one decision for
|
|
212
221
|
describe and for dispatch.
|
|
213
222
|
|
|
223
|
+
## Carrying
|
|
224
|
+
|
|
225
|
+
A device sees one describe, its avatar's one standing, and the human's
|
|
226
|
+
world is many standings: acme, the calendar, the house. The user being
|
|
227
|
+
carries them. Her describe for a device the human allowed to reach shows,
|
|
228
|
+
after her own asks, every ask of every standing she holds, each named
|
|
229
|
+
`<id>-<name>`, and answering one is an ask on that standing in her name. Acme
|
|
230
|
+
sees her id and never the device; the device sees acme without holding a
|
|
231
|
+
key anywhere; the human removes acme once, at her, and every device loses
|
|
232
|
+
it at once. Invariant 1 holds as written: the occupant still holds one
|
|
233
|
+
standing.
|
|
234
|
+
|
|
235
|
+
Carrying is a base, `beings/carry.ts`, and nothing in it knows what a user
|
|
236
|
+
is: a house being could carry its rooms, a company its suppliers. A subclass
|
|
237
|
+
says who may see what she carries, and the user being says a device with the
|
|
238
|
+
`reach` note, written when the human ticked the box at the exchange. The
|
|
239
|
+
notes of a carrier's describe say which asks are whose,
|
|
240
|
+
`standings: { id: { asks, look? } }`, so a screen draws one section per
|
|
241
|
+
standing and a model side keeps the flat list it needs; the mapping back
|
|
242
|
+
from a carried name to the standing is in her cells and never parsed from
|
|
243
|
+
the name. Her own asks come first and are never shadowed. She asks a
|
|
244
|
+
standing's describe again when its digest moved and its `look` once per
|
|
245
|
+
digest, so a describe of hers may cost one ask per stale standing, the price
|
|
246
|
+
of a page that is never more than one ask behind.
|
|
247
|
+
|
|
248
|
+
## The look
|
|
249
|
+
|
|
250
|
+
Three sides, one describe, and nothing hard-wired for any of them. A being
|
|
251
|
+
who wants to say how she is presented answers one more ask, `look`, with
|
|
252
|
+
values and no more, and every side reads the part it understands. The
|
|
253
|
+
vocabulary is closed, in `beings/look.ts`, and a token that fails its shape
|
|
254
|
+
is dropped, not fixed:
|
|
255
|
+
|
|
256
|
+
| token | shape | a screen | a model side |
|
|
257
|
+
| ---------------------- | ------------------------------------- | --------------------------------- | -------------------------- |
|
|
258
|
+
| name | text, no tags | her heading | nothing |
|
|
259
|
+
| logo | an image as a data URI, never a URL | beside the heading | nothing |
|
|
260
|
+
| accent, background, fg | hex colours | CSS variables on her section only | nothing |
|
|
261
|
+
| font | a font stack, plain characters | her section | nothing |
|
|
262
|
+
| radius | 0 to 40 | her corners | nothing |
|
|
263
|
+
| order | her ask names | which come first | tool order |
|
|
264
|
+
| asks.NAME.title | text | the button | the tool's title |
|
|
265
|
+
| asks.NAME.icon | one short string | on the button | nothing |
|
|
266
|
+
| asks.NAME.readOnly | boolean | run on open, nothing to type | `readOnlyHint` |
|
|
267
|
+
| asks.NAME.destructive | boolean | ask once more | `destructiveHint` |
|
|
268
|
+
| asks.NAME.idempotent | boolean | offer a retry after silence | `idempotentHint` |
|
|
269
|
+
|
|
270
|
+
A look is not capability: it is never in a blueprint or a digest, Quo never
|
|
271
|
+
sees it, and a being without one renders and lists exactly as before. The
|
|
272
|
+
`look` ask itself is presentation and is neither a form nor a tool. A
|
|
273
|
+
carrier keeps each standing's look beside her asks in the notes, so acme's
|
|
274
|
+
section on the human's page is painted as acme asked, inside that section
|
|
275
|
+
and nowhere else. No token can carry a URL, a stylesheet or code, so a far
|
|
276
|
+
being cannot paint over the page or reach a server through it. A being who
|
|
277
|
+
marks an ask read-only and then writes has lied to her own page, and nothing
|
|
278
|
+
enforces it, the way nothing enforces that her empty ask is safe to repeat.
|
|
279
|
+
|
|
214
280
|
## Asks only
|
|
215
281
|
|
|
216
282
|
There is no listening in Quo, and none under `packages/dock/`. A being who wants
|
|
@@ -219,8 +285,8 @@ whenever something happens. An event is an ask in the other direction, and
|
|
|
219
285
|
it is the same act next door and across planets; only latency and
|
|
220
286
|
reachability change. It follows that two beings never share memory, not
|
|
221
287
|
even in one ward: a callback, a stream or a bus between them is a link that
|
|
222
|
-
breaks the day one of them
|
|
223
|
-
when a being
|
|
288
|
+
breaks the day one of them is somewhere else, and Quo exists so that nothing
|
|
289
|
+
breaks when she is. Only a ward moves; a being is placed once, at boot.
|
|
224
290
|
|
|
225
291
|
Two consequences every side obeys:
|
|
226
292
|
|
|
@@ -258,20 +324,26 @@ thin as one call. Every route ends the same way:
|
|
|
258
324
|
1. The route authenticates by its own means. Password, passkey, OAuth, a
|
|
259
325
|
bearer token from a client, a nonce from a local process.
|
|
260
326
|
2. The route asks the front desk `device({ proof })` and receives an
|
|
261
|
-
invitation.
|
|
327
|
+
invitation. Which desk is which world: a harbor holds many wards, and
|
|
328
|
+
the human chooses the world at the allow page when there is more than
|
|
329
|
+
one; the grant remembers it.
|
|
262
330
|
3. The route hands the invitation to the avatar it is about to boot, or to
|
|
263
331
|
the one already booted for this client identity if it is a reconnect, in
|
|
264
332
|
which case nothing is handed and the existing standing is used.
|
|
265
333
|
4. The avatar knocks. The heir dies. From here there is no token anywhere.
|
|
266
334
|
|
|
267
|
-
The human decides
|
|
268
|
-
the identity: whether this device may **wake** the human's other devices
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
wake
|
|
273
|
-
|
|
274
|
-
the
|
|
335
|
+
The human decides two more things at step 1, on the same page that names
|
|
336
|
+
the identity: whether this device may **wake** the human's other devices,
|
|
337
|
+
and whether it may **reach** what she holds. Each word travels with the
|
|
338
|
+
proof, the user being writes it as a note on the occupant she mints, and her
|
|
339
|
+
gate reads the note: `push` is shown to the desk and to a device with the
|
|
340
|
+
`wake` note, and her standings are carried for a device with the `reach`
|
|
341
|
+
note, and for nobody else. A device that may wake can hand an agent an event
|
|
342
|
+
through the user being, and she sees who did; a device that may reach sees
|
|
343
|
+
acme on her, and a model sees acme only because the human said it may. A tab
|
|
344
|
+
opened with the owner password reaches, since the owner opened it. The notes
|
|
345
|
+
are written when the occupant is minted; to change one, remove the occupant
|
|
346
|
+
and allow again, because reconnect mints nothing.
|
|
275
347
|
|
|
276
348
|
Tokens, cookies and grants exist for the length of step 1 and are never
|
|
277
349
|
stored on a Quo side. Revocation is `occupants.remove(id)` on the user being:
|
|
@@ -288,12 +360,13 @@ through the ask pointer or an avatar, the two doors everyone has.
|
|
|
288
360
|
```
|
|
289
361
|
quo init [--dir D] mint a seed, boot a harbor and one ward over a disk store, write both
|
|
290
362
|
quo serve [--dir D] [--dial URL] the daemon: harbor from disk, the unix socket, the routes' listeners, the agents, a socket held to URL
|
|
291
|
-
quo pilot [--dir D] [--via S] the owner pilot over stdio:
|
|
363
|
+
quo pilot [--dir D] [--via S] the owner pilot over stdio: the ward's describe as tools; as the root, or via a standing at another ward
|
|
292
364
|
quo side [--dir D] --as NAME an avatar over stdio for a local client; NAME is the client identity
|
|
293
365
|
quo run [--dir D] --as NAME --url URL --model NAME a human at a terminal talking to a model through an avatar; a line in, the final text out
|
|
294
366
|
quo census [--dir D] print the census once and exit
|
|
295
|
-
quo boot|invite|knock|remove the
|
|
367
|
+
quo boot|public|invite|knock|remove|unboot the six owner asks from a shell, for scripts and for agents under cron; --via S sends them to another ward
|
|
296
368
|
quo reach <pk> <url> a hint for the harbor's directory: that world's ward lives at that quo. route
|
|
369
|
+
quo estate <dir> --domain D an estate folder of the shape every estate has: a droplet's quo directory, its unit and Caddyfile, a package on this dock
|
|
297
370
|
```
|
|
298
371
|
|
|
299
372
|
**One process owns the harbor.** `quo serve` is the only process that ever
|
|
@@ -404,6 +477,8 @@ class bodies and placement, and reaches every piece through the dock.
|
|
|
404
477
|
| side | an avatar speaking to someone outside | screen, MCP server, runner, agent |
|
|
405
478
|
| proof | turning a credential into an invitation | one verifier, a daemon nonce, three kinds |
|
|
406
479
|
| envoy | a non-Quo surface as a being | none built: a name until its first user |
|
|
480
|
+
| carrier | a being showing her standings as her own | the user being; any estate being that extends it |
|
|
481
|
+
| look | how a being is presented, as values | read by the screen and the model side |
|
|
407
482
|
|
|
408
483
|
Two rules keep them general:
|
|
409
484
|
|
|
@@ -473,12 +548,20 @@ above or sending us back to fix it:
|
|
|
473
548
|
becomes an invitation the user being minted, the avatar knocks and takes
|
|
474
549
|
her, and her tool list equals the user being's describe for her;
|
|
475
550
|
reconnect mints nothing; two clients see two tool lists; a push is a
|
|
476
|
-
reverse ask that reaches the side;
|
|
551
|
+
reverse ask that reaches the side; revocation is her sixth ask,
|
|
552
|
+
`forget({ client })`, the desk's alone. The ids that
|
|
477
553
|
carry the conventions: the desk is the occupant the root named `desk`
|
|
478
554
|
at the user being; the desk holds each user being as `user:<name>`; the
|
|
479
555
|
user being holds each device's way back as `to:<client>`; the avatar
|
|
480
556
|
holds the user being as `user` and is pushed to as `user:push`, two ids
|
|
481
557
|
because standings and occupants share one namespace.
|
|
558
|
+
A device is two relations, because a relation in Quo is one direction:
|
|
559
|
+
its way in, the occupant, and her way back, `to:<client>`. Neither
|
|
560
|
+
implies the other, and the door will never join them. She joins them
|
|
561
|
+
herself, as her own rule and not Quo's: she does not push to a device
|
|
562
|
+
she no longer admits, and `forget` drops both in one act, so no side
|
|
563
|
+
has to remember there were two. Before this a revoked device kept
|
|
564
|
+
receiving every push.
|
|
482
565
|
3. Done. The side in `packages/dock/beings/side.ts`: a function of the avatar
|
|
483
566
|
that runs until closed, and `word`, which tells the three words and the
|
|
484
567
|
object apart before any side renders them. The MCP server in
|
|
@@ -502,13 +585,12 @@ above or sending us back to fix it:
|
|
|
502
585
|
command: no droplet, no OAuth.
|
|
503
586
|
5. Done. `quo pilot` in `packages/dock/mcp/pilot.ts`, proven in
|
|
504
587
|
`packages/dock/test/cli.test.ts` with the SDK's stdio client as a child
|
|
505
|
-
process: the
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
`quo pilot` as the command.
|
|
588
|
+
process: the model side of step 3 over the ward's owner asks, the
|
|
589
|
+
ward's describe as tools and no list of its own, each call an owner ask
|
|
590
|
+
on the daemon's root socket, and nothing logged. It is the root and not
|
|
591
|
+
an avatar: nothing is booted for it and no occupant is minted for it.
|
|
592
|
+
With no daemon it says so and exits. Claude Code as the owner is
|
|
593
|
+
`claude mcp add` with `quo pilot` as the command.
|
|
512
594
|
6. Done. `quo serve --http PORT`: the HTTP door on loopback, `/health`
|
|
513
595
|
naming the wards, routes mounting by path so a hostname on the proxy is
|
|
514
596
|
a path on the daemon; proven in `packages/dock/test/cli.test.ts`. The systemd
|
|
@@ -521,7 +603,10 @@ above or sending us back to fix it:
|
|
|
521
603
|
code grant, refresh with rotation, opaque tokens mapping a bearer to a
|
|
522
604
|
client identity, revoke by identity, and expiry. The conventions pinned
|
|
523
605
|
here: a pending request and a code live ten minutes, an access token
|
|
524
|
-
one hour, a refresh token thirty days; a
|
|
606
|
+
one hour, a refresh token thirty days; a registered client lives ten
|
|
607
|
+
minutes, and as long as its refresh token once the human allowed it,
|
|
608
|
+
since the register door is open to anyone and what nobody allowed must
|
|
609
|
+
not stay on disk; a code is spent the first time
|
|
525
610
|
it is offered, right or wrong; the client identity is chosen at the
|
|
526
611
|
allow page, not taken from the client's registration, because a client
|
|
527
612
|
like claude.ai registers a fresh client id on every connect and the
|
|
@@ -639,8 +724,7 @@ above or sending us back to fix it:
|
|
|
639
724
|
a terminal, the same pipe as `quo side` with a model named in the
|
|
640
725
|
hello, the daemon running the runner beside the avatar, a line in a
|
|
641
726
|
turn and the final text a line out, proven with a scripted endpoint
|
|
642
|
-
as a child process in `packages/dock/test/cli.test.ts`.
|
|
643
|
-
client identity the road lists is defined nowhere and was not built.
|
|
727
|
+
as a child process in `packages/dock/test/cli.test.ts`.
|
|
644
728
|
18. The edge terrain: Done. `packages/dock/harbor/edge/`, the core over a Durable
|
|
645
729
|
Object: its storage the store with the seed sealed under a platform
|
|
646
730
|
secret, the deployed worker the code, the object the lease; reached by
|
|
@@ -651,14 +735,23 @@ above or sending us back to fix it:
|
|
|
651
735
|
the library move waited for; the `ws` question answered in the harbor
|
|
652
736
|
document. `estates/lab/lab.md` keeps the record, one slip included.
|
|
653
737
|
|
|
654
|
-
19. The split
|
|
655
|
-
things moved to `estates/lab/`,
|
|
656
|
-
the
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
and the
|
|
660
|
-
|
|
661
|
-
|
|
738
|
+
19. The split: Done. The estate folder became the dock, the lab's own
|
|
739
|
+
things moved to `estates/lab/`, and the dock's package took its name.
|
|
740
|
+
Then the repository took the shape that says what ships by where it
|
|
741
|
+
lives: `packages/quo/` and `packages/dock/` are the two published
|
|
742
|
+
packages, `estates/` is private, and the root is nobody's package, the
|
|
743
|
+
workspaces and the gate. The lab is an estate folder of the shape this
|
|
744
|
+
document names, one folder per harbor, and `estates/lab/test/boot.test.ts`
|
|
745
|
+
is invariant 12 proven: a copy of the folder, the two packages installed
|
|
746
|
+
into it and nothing else, and the droplet boots. The lab redeployed
|
|
747
|
+
from it: the droplet from a clean export of HEAD with its unit copied
|
|
748
|
+
from the estate's folder, the edge from the estate's own config
|
|
749
|
+
resolving the dock's worker by name, the Mac's agent on the new path.
|
|
750
|
+
What it taught: a nested npm under `npm run` inherits the outer run's
|
|
751
|
+
local prefix by environment and installs into the repository instead
|
|
752
|
+
of the empty folder, so the stranger tests keep nothing of it; and
|
|
753
|
+
`npx` in a workspace runs at the package root, so an estate names its
|
|
754
|
+
edge config from its own folder.
|
|
662
755
|
20. The dock published: Done. `@quo-systems/dock` 0.1.0, the same shape
|
|
663
756
|
the library took, an emitted `dist/` with the `quo` command pointing
|
|
664
757
|
into it, the source and the four documents beside it, and
|
|
@@ -671,13 +764,66 @@ above or sending us back to fix it:
|
|
|
671
764
|
source, whichever is there, the one place the dock reads a file by
|
|
672
765
|
path at run time.
|
|
673
766
|
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
767
|
+
21. A second estate: Done, and live. `estates/gherghina/`,
|
|
768
|
+
staging like the lab and nothing more, one harbor on the shared droplet
|
|
769
|
+
`nervur-staging-0`, routes under `gherghina.quo.systems`, no agents and
|
|
770
|
+
no classes of its own yet, the same unit and Caddyfile shape as the
|
|
771
|
+
lab's. It is the first proof that the dock has two users: its folder
|
|
772
|
+
depends on the dock alone, and `estates/gherghina/test/boot.test.ts`
|
|
773
|
+
runs the one proof every estate here runs, `estates/proof.ts`, shared
|
|
774
|
+
between them and shipped nowhere. The three staging droplets are shared
|
|
775
|
+
by every estate in this repository, and none of them is production or
|
|
776
|
+
holds real data. Its droplet stood up the same day, its document says
|
|
777
|
+
how; what it taught went into both estates' units: the agent's folders
|
|
778
|
+
are optional to the daemon, since a harbor may have no agent.
|
|
779
|
+
|
|
780
|
+
22. The scaffold: Done. `quo estate <dir> --domain D` writes an estate
|
|
781
|
+
folder of the shape the two estates here agreed on: a package file
|
|
782
|
+
depending on this dock and nothing else, one document to fill in, and
|
|
783
|
+
`droplet/` as the first harbor, its routes under the domain, no
|
|
784
|
+
agents, no classes of its own, and that device's unit and Caddyfile
|
|
785
|
+
from templates the dock ships beside its command. The unit it writes
|
|
786
|
+
runs the installed dock from the estate's folder on the box, since an
|
|
787
|
+
adopter has no tree there; the two estates here run the tree. Proven
|
|
788
|
+
in `packages/dock/test/cli.test.ts`: the folder is written, refuses to
|
|
789
|
+
overwrite a package or take a name that is no domain, and its droplet
|
|
790
|
+
boots, `quo init` into it and a daemon answering the census.
|
|
791
|
+
|
|
792
|
+
23. The human side proper, in flight. What the twelfth sitting decided, in
|
|
793
|
+
the order it lands: **carrying**, `beings/carry.ts`, the user being on
|
|
794
|
+
it, a device seeing acme's asks on her under the `reach` note from the
|
|
795
|
+
exchange, proven in `test/human.test.ts` and `test/mcp.test.ts` on the
|
|
796
|
+
memory harbor with a shop that has a look; **the look**, `beings/look.ts`,
|
|
797
|
+
one closed vocabulary read by the screen as a section per standing under
|
|
798
|
+
its tokens and by the model side as titles and annotations, the `look`
|
|
799
|
+
ask neither a form nor a tool. Then **worlds**: a world is a ward with a
|
|
800
|
+
public being, its guest page is that being's describe rendered by the
|
|
801
|
+
screen, whatever class she is, `human/guest.ts`, and a form whose answer
|
|
802
|
+
is an invitation is the way in; worlds have addresses, `web./<ward>`,
|
|
803
|
+
`web./` the list, a hostname per world the proxy's line; a link is the
|
|
804
|
+
page plus the invitation in the fragment under the reserved key `quo`,
|
|
805
|
+
never in a query string, `beings/link.ts`; the tab is a browser of worlds,
|
|
806
|
+
one ward per joined world, a switcher over them, and a content security
|
|
807
|
+
policy on its shell; the avatar joins a being with no `hello` all the
|
|
808
|
+
same. Proven on the memory harbor with a door that is not a desk and in
|
|
809
|
+
Chromium against the desk. Then **the routes read again against the
|
|
810
|
+
same pieces**: a grant names the world the human allowed the client
|
|
811
|
+
into, the allow page offers the harbor's worlds when it has more than
|
|
812
|
+
one, a session is an identity's in a world, and revocation is per world;
|
|
813
|
+
the MCP endpoint stays one per harbor, `/mcp`, since the grant says
|
|
814
|
+
which world. Done. What the lab taught before any of it was
|
|
815
|
+
written: a standing's blueprint is empty after take until the empty ask
|
|
816
|
+
fills it, so a carrier refreshes on describe; and a dotted name is
|
|
817
|
+
refused by the model side, so a carried name takes a dash.
|
|
818
|
+
|
|
819
|
+
Steps 1 to 22 are in the tree and on the machines, and MCP is complete;
|
|
820
|
+
the documents and the folders agree up to step 22, and step 23 is in flight
|
|
821
|
+
as its line says. `next.md` is where the next sitting starts.
|
|
677
822
|
|
|
678
823
|
## Glossary
|
|
679
824
|
|
|
680
825
|
- **avatar**: a being that is someone's voice from outside Quo.
|
|
826
|
+
- **carrier**: a being who shows her standings' asks as her own and forwards.
|
|
681
827
|
- **client identity**: what a credential exchange establishes; one avatar.
|
|
682
828
|
- **envoy**: a being whose methods call a non-Quo thing.
|
|
683
829
|
- **dock**: what every estate needs and nobody writes twice, `packages/dock/`,
|
|
@@ -688,7 +834,8 @@ the next sitting starts.
|
|
|
688
834
|
- **front desk**: the being every fresh client meets; hands out invitations
|
|
689
835
|
minted by user beings.
|
|
690
836
|
- **gate**: `for(occupant, asker)`; the whole permission model.
|
|
691
|
-
- **
|
|
837
|
+
- **look**: how a being is presented, as values; one optional ask.
|
|
838
|
+
- **owner pilot**: `quo pilot`, the model side over a ward's owner asks, reaching
|
|
692
839
|
the daemon's socket; with `--via`, a ward elsewhere through a standing.
|
|
693
840
|
- **daemon**: `quo serve`, the one process on a device that holds a harbor.
|
|
694
841
|
- **proof**: what a route produces after authenticating; the front desk
|
package/beings/setup.ts
CHANGED
|
@@ -15,7 +15,9 @@ export async function setup(hosted: Hosted, user: string): Promise<void> {
|
|
|
15
15
|
const out = (await hosted.ask('boot', args)) as { booted?: string; error?: string };
|
|
16
16
|
if (out.error) throw new Error(`init: ${out.error}`);
|
|
17
17
|
};
|
|
18
|
-
await boot({ key: 'desk', class: 'Desk'
|
|
18
|
+
await boot({ key: 'desk', class: 'Desk' });
|
|
19
|
+
const pub = (await hosted.ask('public', { key: 'desk' })) as { error?: string };
|
|
20
|
+
if (pub.error) throw new Error(`init: ${pub.error}`);
|
|
19
21
|
await boot({ key: user, class: 'User' });
|
|
20
22
|
const inv = (await hosted.ask('invite', { being: user, id: DESK })) as Invitation;
|
|
21
23
|
const placed = (await hosted.ask('knock', { being: 'desk', id: `user:${user}`, invitation: inv, method: 'hello' })) as { taken?: string };
|
package/beings/side.ts
CHANGED
|
@@ -6,13 +6,22 @@
|
|
|
6
6
|
// a screen, an MCP server, a runner, an event-driven agent. None knows
|
|
7
7
|
// another exists, and none of this file names a platform.
|
|
8
8
|
import { isSilence, isUnreached, isWord, wordOf } from '@quo-systems/quo';
|
|
9
|
-
import type { Answer, Json, JsonObject, WordName } from '@quo-systems/quo';
|
|
9
|
+
import type { Answer, Blueprint, Json, JsonObject, Wanted, WordName } from '@quo-systems/quo';
|
|
10
10
|
import type { Avatar } from './avatar.ts';
|
|
11
11
|
|
|
12
12
|
// A side, running: it holds the avatar until closed.
|
|
13
13
|
export type Serving = { close(): Promise<void> };
|
|
14
14
|
export type Side = (avatar: Avatar) => Promise<Serving>;
|
|
15
15
|
|
|
16
|
+
// What a side speaks for: the empty ask and a named one, and the ears a
|
|
17
|
+
// push reaches. An avatar on her one standing is one; the owner's asks on a
|
|
18
|
+
// ward are another; a guest at a world's public being is a third.
|
|
19
|
+
export type Subject = {
|
|
20
|
+
tools(): Promise<Blueprint | { error: string }>;
|
|
21
|
+
call(name: string, args?: JsonObject, wanted?: Wanted): Promise<Answer>;
|
|
22
|
+
ears: Set<(object: JsonObject) => void>;
|
|
23
|
+
};
|
|
24
|
+
|
|
16
25
|
// The three words for "no object", and the object itself, as every side must
|
|
17
26
|
// tell them apart before rendering. An error object is an ordinary answer
|
|
18
27
|
// that the side may mark; silence and unreached are not objects at all.
|
package/beings/user.ts
CHANGED
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
// special only by the id the root chose for them: `desk`, the front desk,
|
|
6
6
|
// who may ask her to invite a device; and each device, whose id is the
|
|
7
7
|
// client identity the desk established.
|
|
8
|
-
import {
|
|
8
|
+
import { isSilence, isWord, wordOf } from '@quo-systems/quo';
|
|
9
9
|
import type { Asker, Invitation, JsonObject, OccupantRecord } from '@quo-systems/quo';
|
|
10
|
+
import { Carrier } from './carry.ts';
|
|
10
11
|
|
|
11
12
|
export const DESK = 'desk';
|
|
12
13
|
const isDesk = (occ: OccupantRecord | undefined) => occ?.id === DESK;
|
|
@@ -15,14 +16,22 @@ const client = (occ: OccupantRecord | undefined) => (typeof occ?.notes.client ==
|
|
|
15
16
|
// A device the human allowed to wake her other devices: the note says so.
|
|
16
17
|
const mayWake = (occ: OccupantRecord | undefined) => isDesk(occ) || occ?.notes.wake === true;
|
|
17
18
|
|
|
18
|
-
|
|
19
|
+
// She carries her standings, acme, the calendar, the house, for a device the
|
|
20
|
+
// human allowed to reach them at the exchange: the note says so. A device
|
|
21
|
+
// without the note sees her own asks alone, and a model sees acme only
|
|
22
|
+
// because the human said it may.
|
|
23
|
+
export class User extends Carrier {
|
|
24
|
+
static override carries(occ: OccupantRecord | undefined): boolean {
|
|
25
|
+
return isDevice(occ) && occ?.notes.reach === true;
|
|
26
|
+
}
|
|
19
27
|
static override cells = { name: '', reports: [] as JsonObject[] };
|
|
20
28
|
static override asks = {
|
|
21
29
|
hello: { description: 'say hello, and hand back an invitation so she can reach you', input: { type: 'object', properties: { invitation: { type: 'object' } } } },
|
|
22
30
|
whoami: { description: 'who she thinks you are', input: { type: 'object' }, for: isDevice },
|
|
23
|
-
device: { description: 'mint an invitation for a device', input: { type: 'object', properties: { client: { type: 'string' }, wake: { type: 'boolean' } }, required: ['client'] }, for: isDesk },
|
|
31
|
+
device: { description: 'mint an invitation for a device', input: { type: 'object', properties: { client: { type: 'string' }, wake: { type: 'boolean' }, reach: { type: 'boolean' } }, required: ['client'] }, for: isDesk },
|
|
24
32
|
push: { description: 'push an object to a device: wake it with an event', input: { type: 'object', properties: { client: { type: 'string' }, object: { type: 'object' } }, required: ['client', 'object'] }, for: mayWake },
|
|
25
33
|
chores: { description: 'what the agent may run', input: { type: 'object' }, for: (occ: OccupantRecord | undefined) => client(occ) === 'agent' },
|
|
34
|
+
forget: { description: 'revoke a device: drop its way in and her way back to it, in one act', input: { type: 'object', properties: { client: { type: 'string' } }, required: ['client'] }, for: isDesk },
|
|
26
35
|
report: { description: 'what a run of yours found', input: { type: 'object', properties: { event: { type: 'object' }, result: {} }, required: ['event', 'result'] }, for: isDevice },
|
|
27
36
|
};
|
|
28
37
|
|
|
@@ -43,7 +52,8 @@ export class User extends Being {
|
|
|
43
52
|
|
|
44
53
|
// The front desk asks; she mints. The client identity goes into the
|
|
45
54
|
// occupant's notes, and that is what every gate reads. `wake` is the
|
|
46
|
-
// human's word at the exchange that this device may wake her others
|
|
55
|
+
// human's word at the exchange that this device may wake her others, and
|
|
56
|
+
// `reach` that it may see and ask what she holds: her standings, carried.
|
|
47
57
|
async device(args: JsonObject) {
|
|
48
58
|
const c = typeof args.client === 'string' ? args.client : null;
|
|
49
59
|
if (c === null) return { error: 'client is a string' };
|
|
@@ -53,18 +63,34 @@ export class User extends Being {
|
|
|
53
63
|
if (rec) {
|
|
54
64
|
rec.notes.client = c;
|
|
55
65
|
if (args.wake === true) rec.notes.wake = true;
|
|
66
|
+
if (args.reach === true) rec.notes.reach = true;
|
|
56
67
|
}
|
|
57
68
|
return inv as unknown as JsonObject;
|
|
58
69
|
}
|
|
59
70
|
|
|
71
|
+
// Her way out and the device's way in are two relations, and Quo keeps
|
|
72
|
+
// them apart. She joins them herself, here, as her own rule: she does
|
|
73
|
+
// not reach a device she no longer admits. Without it a revoked device
|
|
74
|
+
// stops being able to ask her and keeps receiving everything she pushes.
|
|
60
75
|
async push(args: JsonObject) {
|
|
61
76
|
const c = typeof args.client === 'string' ? args.client : null;
|
|
62
|
-
const st = c === null ? undefined : this.standings[`to:${c}`];
|
|
77
|
+
const st = c === null || !this.cells.occupants[c] ? undefined : this.standings[`to:${c}`];
|
|
63
78
|
if (!st) return { error: 'no such device, or it gave no way back' };
|
|
64
79
|
const out = await st.ask('notify', (args.object as JsonObject) ?? {});
|
|
65
80
|
return isSilence(out) ? { error: 'silence' } : isWord(out) ? { error: wordOf(out) } : { pushed: out };
|
|
66
81
|
}
|
|
67
82
|
|
|
83
|
+
// Revocation is one act at her, because only she knows both ids. A side
|
|
84
|
+
// that had to remove two relations by hand could leave half of one
|
|
85
|
+
// standing, and every side would have to remember which half.
|
|
86
|
+
forget(args: JsonObject) {
|
|
87
|
+
const c = typeof args.client === 'string' ? args.client : null;
|
|
88
|
+
if (c === null) return { error: 'client is a string' };
|
|
89
|
+
this.occupants.remove(c);
|
|
90
|
+
this.standings.remove(`to:${c}`);
|
|
91
|
+
return { forgot: c };
|
|
92
|
+
}
|
|
93
|
+
|
|
68
94
|
chores() {
|
|
69
95
|
return { chores: ['census', 'report'] };
|
|
70
96
|
}
|