@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.
Files changed (72) hide show
  1. package/README.md +9 -2
  2. package/beings/avatar.ts +6 -2
  3. package/beings/carry.ts +101 -0
  4. package/beings/desk.ts +2 -2
  5. package/beings/index.ts +2 -0
  6. package/beings/link.ts +54 -0
  7. package/beings/look.ts +96 -0
  8. package/beings/quo-dock.md +185 -38
  9. package/beings/setup.ts +3 -1
  10. package/beings/side.ts +10 -1
  11. package/beings/user.ts +31 -5
  12. package/cli/daemon.ts +98 -44
  13. package/cli/estate/Caddyfile +25 -0
  14. package/cli/estate/quo.service +36 -0
  15. package/cli/estate.ts +44 -0
  16. package/cli/quo.ts +14 -3
  17. package/dist/beings/avatar.js +7 -2
  18. package/dist/beings/carry.d.ts +10 -0
  19. package/dist/beings/carry.js +106 -0
  20. package/dist/beings/desk.d.ts +1 -0
  21. package/dist/beings/desk.js +1 -1
  22. package/dist/beings/index.d.ts +2 -0
  23. package/dist/beings/index.js +2 -0
  24. package/dist/beings/link.d.ts +7 -0
  25. package/dist/beings/link.js +42 -0
  26. package/dist/beings/look.d.ts +27 -0
  27. package/dist/beings/look.js +71 -0
  28. package/dist/beings/setup.js +4 -1
  29. package/dist/beings/side.d.ts +8 -1
  30. package/dist/beings/user.d.ts +26 -2
  31. package/dist/beings/user.js +32 -5
  32. package/dist/cli/daemon.d.ts +1 -1
  33. package/dist/cli/daemon.js +98 -44
  34. package/dist/cli/estate/Caddyfile +25 -0
  35. package/dist/cli/estate/quo.service +36 -0
  36. package/dist/cli/estate.d.ts +5 -0
  37. package/dist/cli/estate.js +46 -0
  38. package/dist/cli/quo.js +14 -3
  39. package/dist/harbor/edge/exercise.js +3 -1
  40. package/dist/harbor/edge/platform.d.ts +28 -0
  41. package/dist/human/guest.d.ts +3 -0
  42. package/dist/human/guest.js +25 -0
  43. package/dist/human/html.d.ts +10 -2
  44. package/dist/human/html.js +61 -10
  45. package/dist/human/screen.d.ts +8 -3
  46. package/dist/human/screen.js +25 -6
  47. package/dist/human/tab.d.ts +2 -0
  48. package/dist/human/tab.js +127 -42
  49. package/dist/mcp/http.d.ts +4 -3
  50. package/dist/mcp/http.js +6 -6
  51. package/dist/mcp/oauth.d.ts +11 -4
  52. package/dist/mcp/oauth.js +32 -21
  53. package/dist/mcp/pilot.d.ts +3 -4
  54. package/dist/mcp/pilot.js +19 -65
  55. package/dist/mcp/server.d.ts +6 -4
  56. package/dist/mcp/server.js +48 -11
  57. package/dist/mcp/web/exchange.d.ts +5 -2
  58. package/dist/mcp/web/exchange.js +21 -7
  59. package/harbor/edge/exercise.ts +2 -1
  60. package/harbor/quo-harbor.md +37 -0
  61. package/human/guest.ts +26 -0
  62. package/human/html.ts +58 -10
  63. package/human/quo-human.md +123 -66
  64. package/human/screen.ts +28 -7
  65. package/human/tab.ts +153 -51
  66. package/mcp/http.ts +10 -9
  67. package/mcp/oauth.ts +39 -23
  68. package/mcp/pilot.ts +26 -65
  69. package/mcp/quo-mcp.md +34 -19
  70. package/mcp/server.ts +52 -19
  71. package/mcp/web/exchange.ts +23 -9
  72. package/package.json +7 -3
@@ -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. serves the bundle, runs the credential exchange, ends with an invitation
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 moves, and Quo exists so that nothing breaks
223
- when a being moves.
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 one more thing at step 1, on the same page that names
268
- the identity: whether this device may **wake** the human's other devices.
269
- The word travels with the proof, the user being writes it as a note on the
270
- occupant she mints, and her gate reads the note: `push` is shown to the
271
- desk and to a device with that note, and to nobody else. A device that may
272
- wake can hand an agent an event through the user being, and she sees who
273
- did. The note is written when the occupant is minted; to change it, remove
274
- the occupant and allow again, because reconnect mints nothing.
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: boot, invite, knock, remove, census; as the root, or via a standing at another ward
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 four owner asks from a shell, for scripts and for agents under cron; --via S sends them to another ward
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; remove is revocation. The ids that
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 owner's asks as tools and no more, census, boot, invite,
506
- knock and, since step 16, remove, each
507
- one an owner ask on the daemon's root socket, logged to stderr with
508
- what it made. It is the root and not an avatar: nothing is booted for
509
- it and no occupant is minted for it. With no daemon it says so and
510
- exits. Claude Code as the owner is `claude mcp add` with
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 code is spent the first time
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`. The gate profile per
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, in flight: the estate folder became the dock, the lab's own
655
- things moved to `estates/lab/`, its units and its document, and
656
- the dock's package took its name. Then the repository took the shape
657
- that says what ships by where it lives: `packages/quo/` and
658
- `packages/dock/` are the two published packages, `estates/` is private,
659
- and the root is nobody's package, the workspaces and the gate. The lab
660
- redeployed from the new paths and a copy of `estates/lab/` booting in
661
- an empty repository are what close it.
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
- Steps 1 to 18 and 20 are in the tree and on the lab, and MCP is complete;
675
- step 19 is in flight, and it is the one gap named today. `next.md` is where
676
- the next sitting starts.
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
- - **owner pilot**: `quo pilot`, the four owner asks over stdio, reaching
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', public: true });
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 { Being, isSilence, isWord, wordOf } from '@quo-systems/quo';
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
- export class User extends Being {
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
  }