@quo-systems/dock 0.2.1 → 0.2.2

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/beings/avatar.ts CHANGED
@@ -4,7 +4,9 @@
4
4
  // exactly one standing, `user`, and everything her side asks goes through
5
5
  // it; the user being reaches her only to push, and she hands the push to
6
6
  // every side she has. A side is in-process with her, holds the object, and
7
- // calls the three methods below. Nothing of a side is in her cells.
7
+ // calls the four methods below: one of the two ways in, `enter` behind a
8
+ // route's proof or `join` with an invitation, then `tools` and `call`.
9
+ // Nothing of a side is in her cells.
8
10
  import { Being, isSilence, isWord, wordOf } from '@quo-systems/quo';
9
11
  import type { Answer, Blueprint, Invitation, JsonObject, OccupantRecord, Wanted } from '@quo-systems/quo';
10
12
 
package/beings/index.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
- // The beings every side shares. See quo-estate.md, the trunk.
2
+ // The beings every side shares. See quo-dock.md, the trunk.
3
3
  export { User, DESK } from './user.ts';
4
4
  export { Desk, type Proof, type Verified, type Verifier } from './desk.ts';
5
5
  export { Avatar, USER, PUSHER } from './avatar.ts';
@@ -124,6 +124,31 @@ that every arrival is named and judged.
124
124
  - **Route.** A hostname a reverse proxy sends to one process: `web.`, `quo.`,
125
125
  `mcp.`. Routes are deployment, never boundaries.
126
126
 
127
+ ## The ids
128
+
129
+ Every relation here is named by an id one being minted, and the dock's own
130
+ are these. They are conventions of this package and nothing of Quo's; a
131
+ different estate could choose others and change no word of the spec.
132
+
133
+ | id | who holds it | what it names |
134
+ | ------------------- | -------------- | ---------------------------------------- |
135
+ | `desk` | the user being | the front desk, her privileged occupant |
136
+ | `user:<name>` | the front desk | its standing to that human's user being |
137
+ | `<client>` | the user being | one device, by its client identity |
138
+ | `to:<client>` | the user being | her way back to that device |
139
+ | `user` | an avatar | her one standing, at the user being |
140
+ | `user:push` | an avatar | the user being as her occupant, to push |
141
+ | `avatar:<identity>` | the ward | the being key one client identity boots |
142
+
143
+ A device is two relations, because a relation in Quo is one direction: its
144
+ way in, the occupant, and her way back, `to:<client>`. Neither implies the
145
+ other and the door never joins them. The user being joins them herself, as
146
+ her own rule: she does not push to a device she no longer admits, and
147
+ `forget` drops both in one act. Her way back is handed over at hello and
148
+ taken when it can be: a knock back that is unreached, a tab whose pk the
149
+ far harbor has not bound yet or a phone in a tunnel, spends nothing, so she
150
+ keeps the invitation and takes it at the next push. A refusal is final.
151
+
127
152
  ## Architecture
128
153
 
129
154
  One droplet, one harbor, three routes. Every other placement is a subset.
@@ -173,6 +198,15 @@ standing to the user being is minted once and persists in the partition; a
173
198
  side opening and closing is a reconnect, and the keys carry on. Reconnect
174
199
  never re-invites.
175
200
 
201
+ An avatar is one standing, and a harbor holds as many avatars as it holds
202
+ relations; a ward only groups them. The droplet groups by client identity
203
+ per world, in the avatars ward. A tab groups by world, one local ward per
204
+ far world and one avatar per relation in it, so a fresh invitation is
205
+ always a fresh avatar and never a join on one that already holds hers: two
206
+ invitations into one world are two avatars, and two humans on one family
207
+ tablet are two relations. Which avatar a side speaks through is a switch
208
+ and nothing more.
209
+
176
210
  One class for every side. It has:
177
211
 
178
212
  - `answer`: the user being asks her only to push, and she forwards the push
@@ -211,8 +245,8 @@ last word. A push is the trigger of a turn, never a line smuggled into one:
211
245
  a push that arrives while a turn is running waits in her cells and starts
212
246
  the next turn when this one ends. Nothing is dropped and nothing is held;
213
247
  the ask that carried the push was answered the moment it landed. Between
214
- turns, the world reaches a model the way step 11 says, an ask that starts a
215
- run and a callback when it ends.
248
+ turns, the world reaches a model as `packages/dock/mcp/quo-mcp.md` says: an
249
+ ask that starts a run, and a callback when it ends.
216
250
 
217
251
  One describe, many renderings. A screen is the user being's describe spoken
218
252
  as forms; a tool list is the same describe spoken as tools. Nobody builds a
@@ -340,12 +374,32 @@ gate reads the note: `push` is shown to the desk and to a device with the
340
374
  `wake` note, and her standings are carried for a device with the `reach`
341
375
  note, and for nobody else. A device that may wake can hand an agent an event
342
376
  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.
347
-
348
- Tokens, cookies and grants exist for the length of step 1 and are never
377
+ acme on her, and a model sees acme only because the human said it may. The
378
+ notes are written when the occupant is minted; to change one, remove the
379
+ occupant and allow again, because reconnect mints nothing. A device the
380
+ root mints on her directly, with `quo invite`, carries no note: the id the
381
+ root chose is its client identity, and it reaches and may wake, since the
382
+ root owns everything she holds and chose to mint it.
383
+
384
+ Two proof kinds exist, and each arrives only the one way it is made:
385
+ `local`, a side that reached the daemon's side socket; `web`, a human who
386
+ passed the exchange pages. Each is a nonce the daemon minted for one kind
387
+ and honours once, under that kind alone, so a proof of any other provenance
388
+ names no nonce and is refused. A human at a screen passes no exchange: a
389
+ tab enters by a link, an invitation a user being minted, and nothing is
390
+ ever typed to get in. The root's first device is no exception; the root
391
+ mints its invitation with `quo invite` on the box and opens the link.
392
+
393
+ The route's own lifetimes, where it speaks OAuth: a pending request and a
394
+ code live ten minutes, an access token one hour, a refresh token thirty
395
+ days. A registered client lives ten minutes, and as long as its refresh
396
+ token once the human allowed it, since the register door is open to anyone
397
+ and what nobody allowed must not stay on disk. A code is spent the first
398
+ time it is offered, right or wrong. The client identity is chosen by the
399
+ human at the allow page and never taken from the client's registration: a
400
+ client may register afresh on every connect, and the avatar outlives that.
401
+
402
+ Tokens, cookies and grants exist for the length of step 1 above and are never
349
403
  stored on a Quo side. Revocation is `occupants.remove(id)` on the user being:
350
404
  the client's next ask, under the key the door bound for its avatar, hears
351
405
  `removed`, and the side speaks that in its own vocabulary: the MCP route
@@ -355,7 +409,11 @@ answers 401 at the next session and forgets the grant.
355
409
 
356
410
  One command, `quo`, under `packages/dock/cli/`. It imports the four library exports
357
411
  and nothing else, holds no class body, and never speaks to a ward except
358
- through the ask pointer or an avatar, the two doors everyone has.
412
+ through the ask pointer or an avatar, the two doors everyone has. The
413
+ daemon's folder holds the process and its doors, the two sockets and the
414
+ HTTP listener; each route on that listener lives in the folder whose truth
415
+ it is, `human/web.ts`, `mcp/route.ts`, `harbor/quo.ts`, and the daemon only
416
+ mounts them.
359
417
 
360
418
  ```
361
419
  quo init [--dir D] mint a seed, boot a harbor and one ward over a disk store, write both
@@ -465,8 +523,8 @@ to be decided.
465
523
 
466
524
  ## Generality
467
525
 
468
- Six general pieces exist under `packages/dock/`, and everything concrete is an
469
- implementation of one of them. An estate implements none of them: it holds
526
+ Eight general pieces exist under `packages/dock/`, and everything concrete is
527
+ an implementation of one of them. An estate implements none of them: it holds
470
528
  class bodies and placement, and reaches every piece through the dock.
471
529
 
472
530
  | piece | one interface for | implementations |
@@ -489,8 +547,8 @@ Two rules keep them general:
489
547
 
490
548
  Harbor core, reach and store held across three terrains and moved into
491
549
  `packages/quo/src` with `packages/quo/SPEC.md` in the same commit on 2026-09-04;
492
- what stays here of them is each terrain's store, loader, lease and listener. The
493
- other three are dock conventions and stay here.
550
+ what stays here of them is each terrain's store, loader, lease and listener.
551
+ The other five are dock conventions and stay here.
494
552
 
495
553
  ## Invariants
496
554
 
@@ -518,8 +576,11 @@ other three are dock conventions and stay here.
518
576
 
519
577
  ## Where the dock stands
520
578
 
521
- The one place under `packages/dock/` where a gap between these documents and the
522
- folders is named. Each line is a debt to close, not a note to keep.
579
+ The one place under `packages/dock/` where a gap between these documents and
580
+ the folders is named. Each line is a debt to close, not a note to keep.
581
+
582
+ No gap today. Everything these documents describe is in the folders and on
583
+ the machines, and MCP is complete.
523
584
 
524
585
  Two rules hold:
525
586
 
@@ -533,292 +594,7 @@ Two rules hold:
533
594
  keep in step with every change, and the vectors under `packages/quo/vectors/`
534
595
  are the hand that will be extended when the time comes.
535
596
 
536
- The order of work, each step usable on its own and each proving a sentence
537
- above or sending us back to fix it:
538
-
539
- 1. Done. `packages/dock/` scaffold: its own package linking the library, the
540
- four exports only, `check:dock` in the root gate, the terrain test pointed at
541
- `packages/dock/beings`. The disk harbor in `packages/dock/harbor/disk.ts`:
542
- one folder per ward holding seed, partition and the class source, a ward
543
- record, the lease, and a save after every call. The `quo` CLI: init, serve,
544
- census, boot, invite, knock, over the daemon's unix socket. Pilot and side
545
- landed at steps 4 and 5.
546
- 2. Done. The beings in `packages/dock/beings/`: `User`, `Desk`, `Avatar`, proven
547
- on the memory harbor in `packages/dock/test/beings.test.ts`. A fake proof
548
- becomes an invitation the user being minted, the avatar knocks and takes
549
- her, and her tool list equals the user being's describe for her;
550
- reconnect mints nothing; two clients see two tool lists; a push is a
551
- reverse ask that reaches the side; revocation is her sixth ask,
552
- `forget({ client })`, the desk's alone. The ids that
553
- carry the conventions: the desk is the occupant the root named `desk`
554
- at the user being; the desk holds each user being as `user:<name>`; the
555
- user being holds each device's way back as `to:<client>`; the avatar
556
- holds the user being as `user` and is pushed to as `user:push`, two ids
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.
565
- 3. Done. The side in `packages/dock/beings/side.ts`: a function of the avatar
566
- that runs until closed, and `word`, which tells the three words and the
567
- object apart before any side renders them. The MCP server in
568
- `packages/dock/mcp/server.ts`, proven over the SDK's in-memory transport in
569
- `packages/dock/test/mcp.test.ts`: tools/list is her describe spoken as tools,
570
- tools/call is a named ask, an object is a result, an error object and
571
- the two words are error results with the text the trunk gives them, a
572
- push is a logging notification, and a session closing and opening
573
- reuses the standing. The SDK is the dock's first dependency, under
574
- `packages/dock/` alone.
575
- 4. Done. `quo side --as NAME` over stdio, proven in
576
- `packages/dock/test/cli.test.ts` with the SDK's stdio client as a child
577
- process: the daemon finds or boots
578
- `avatar:NAME`, enters her with a nonce the desk honours once, and runs
579
- the model side beside her; the tool list is the user being's describe
580
- for that client; a second start is a reconnect and mints nothing; a
581
- `local` proof from anywhere else is refused. `quo init` now does the
582
- root's setup, the desk public and one user being named by `--user`, so
583
- a fresh folder is a world. Claude Code and Claude Desktop on the Mac
584
- need only `quo serve` running and `quo side --as claude` as the server
585
- command: no droplet, no OAuth.
586
- 5. Done. `quo pilot` in `packages/dock/mcp/pilot.ts`, proven in
587
- `packages/dock/test/cli.test.ts` with the SDK's stdio client as a child
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.
594
- 6. Done. `quo serve --http PORT`: the HTTP door on loopback, `/health`
595
- naming the wards, routes mounting by path so a hostname on the proxy is
596
- a path on the daemon; proven in `packages/dock/test/cli.test.ts`. The systemd
597
- unit and the Caddyfile are in `estates/lab/droplet/`. Disk store, the
598
- lease and reboot from disk with relations intact were steps 1 and 4.
599
- 7. Done. OAuth on the `mcp.` route in `packages/dock/mcp/oauth.ts`, mounted by
600
- the daemon when `<dir>/routes.json` names the two public origins, and
601
- proven over real HTTP in `packages/dock/test/oauth.test.ts`: metadata,
602
- protected-resource metadata, dynamic registration, PKCE with S256, the
603
- code grant, refresh with rotation, opaque tokens mapping a bearer to a
604
- client identity, revoke by identity, and expiry. The conventions pinned
605
- here: a pending request and a code live ten minutes, an access token
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
610
- it is offered, right or wrong; the client identity is chosen at the
611
- allow page, not taken from the client's registration, because a client
612
- like claude.ai registers a fresh client id on every connect and the
613
- avatar must outlive that. The store is `<dir>/oauth.json`, the route's
614
- own, mode 0600, and holds no secret of Quo's.
615
- 8. Done. The exchange pages in `packages/dock/mcp/web/exchange.ts`, mounted on
616
- the web route, proven over real HTTP in `packages/dock/test/web.test.ts`.
617
- Login checks `QUO_OWNER_PASSWORD` in constant time and sets a signed
618
- session cookie that lives ten minutes, the life of a request; with no
619
- password set the exchange is closed. Allow names the client, where it
620
- returns to, and the identity it will be, suggested from the client's
621
- name and editable, one word and not the user or the desk. Allow admits
622
- her through the one path every side uses: find or boot the avatar,
623
- enter with a nonce the desk honours once under the `web` proof kind,
624
- then complete the OAuth request with that identity. Deny sends the
625
- client `access_denied`. Two pages, hand-written, rendering no blueprint.
626
- 9. Done. The MCP endpoint in `packages/dock/mcp/http.ts` at `/mcp` on the mcp
627
- route: streamable HTTP, a bearer names the identity, the identity names
628
- her avatar through the same admit path, the side from step 3 runs
629
- beside her, one session per connection, a session belonging to the
630
- identity that opened it. Proven in `packages/dock/test/http.test.ts` with the
631
- SDK's streamable HTTP client over the whole exchange. MCP is complete:
632
- every human-assisted case on every device, with nothing of Quo's
633
- crossing a machine.
634
- 10. Done. The runner in `packages/dock/mcp/runner.ts`: the second side, an MCP
635
- client written inline for a model that speaks function calling. Her
636
- describe is a tools array, a tool call is a named ask, the three words
637
- are tool messages, a line from the human or a push from the user being
638
- starts a turn, and turns never overlap. Proven in
639
- `packages/dock/test/runner.test.ts` over a scripted endpoint on loopback, and
640
- live against Qwen3 Coder 30B in LM Studio on the Mac, which read her
641
- describe, called `whoami` through the gate and answered with the id she
642
- minted. What it taught: LM Studio refuses a parameters schema with no
643
- `properties`, so the runner is where a describe is narrowed for an
644
- endpoint, and nowhere else.
645
- 11. Done. The agent side in `packages/dock/mcp/agent.ts`: the world is the loop.
646
- A push from the user being is acknowledged at once by her `notify`,
647
- lands in her inbox, which is the queue in her cells, and the side runs
648
- it, one event at a time in order, then asks the user being back with
649
- `report({ event, result })`, the ask this step added to her, gated to
650
- devices. A run is a function of the event or a process in the agent's
651
- folder with the event as its last argument, which is what `claude -p`
652
- is. Proven in `packages/dock/test/agent.test.ts` with a fake run and a fake
653
- process: the ask is answered before the run ends, a throw reports an
654
- error object, and events that came while no side was up run when one
655
- comes up. What it decided: the queue lives in the partition and not in
656
- the side, so a restart loses nothing but the one run in flight. On a
657
- device, `<dir>/agents.json` names each agent's identity, command and
658
- folder, and the daemon admits her on the local proof and runs her side
659
- beside her avatar; proven in the same suite over a disk harbor.
660
- 12. The `quo.` route: Done. The reach interface with request and socket, since
661
- the library move `packages/quo/src/harbor/reach.ts`, the disk harbor holds
662
- the directory, and the daemon mounts `/quo` and dials with `--dial`. The
663
- library's conformance suite passed untouched over two disk harbors on
664
- loopback under both reaches, and the rendezvous carried a knock from a third
665
- harbor to a dialer through its listener; `packages/dock/test/wire.test.ts`.
666
- Live: a Mac daemon dialed `quo.lab.quo.systems`, an avatar there knocked at
667
- the lab's user being across the internet, the lab knocked back over the held
668
- socket and took `to:mac`, and a push from the lab landed in her inbox on the
669
- Mac. What it taught: the proxy rewrites the route root with a trailing
670
- slash, and a ward pk is 64 bytes on the wire;
671
- `packages/dock/harbor/quo-harbor.md` has both.
672
- 13. The browser harbor: Done. Harbor core carved out of the disk harbor
673
- into a core of its own, since the library move `packages/quo/src/harbor/core.ts`,
674
- the store a piece with files and IndexedDB under it, the dialer its
675
- own terrain-free piece, the socket reach on the standard surface.
676
- `packages/dock/harbor/browser.ts` is the core in a tab: IndexedDB, a web lock
677
- for the lease, one socket per world.
678
- The library's conformance suite passed untouched inside a real
679
- Chromium against two browser harbors in one tab through a daemon on
680
- loopback, migration between two databases included;
681
- `packages/dock/test/terrain/browser.test.ts`, behind `check:terrain`. And the
682
- first screen, `packages/dock/human/tab.ts`, rudimentary on purpose: a page on
683
- the web route that boots a harbor in the tab, takes the owner password
684
- once under the `tab` proof kind, and renders her describe as buttons,
685
- with pushes landing on the page; a reload asks nothing. What it taught:
686
- a tab's store keeps values through JSON because the cells guard refuses
687
- structured clone; a side in a tab saves after the exchange and after
688
- every call, as the daemon does; and a dialed socket is the fallback
689
- from the moment it is dialed, because a ward asks the instant it boots.
690
- 14. Owners at the door in the CLI: Done. The root socket takes `via`, a
691
- standing the user being holds at another ward, and the daemon asks on
692
- it in-process and saves; `quo pilot` and the bare owner asks take
693
- `--via S`. Scene 8 on two daemons over a dialed socket in
694
- `packages/dock/test/cli.test.ts`: the far root invites on its ward's pk, the
695
- user being here knocks and takes `lab`, census and boot land there,
696
- invite on the far pk is refused at the door, the standing survives a
697
- restart, and the owner removed meets silence. What it taught: the
698
- library had no owner ask that removes an owner, and the test reached
699
- into the ward's cells; the word is `remove` now, decided in `packages/quo/SPEC.md`
700
- at step 16.
701
- 15. The human side, minimal: Done. Three pieces under `packages/dock/human/`:
702
- `html.ts`, pure, a schema as a form, a form back as typed arguments,
703
- an answer as a view, the three words with faces; `screen.ts`, the side
704
- over a surface that can only show a page and hand back a form,
705
- re-asking her describe after every call so the page follows the
706
- digest; `dom.ts`, the surface on an element. The tab keeps its harbor
707
- and exchange and hands the avatar to the side. Proven on the memory
708
- harbor with a fake surface in `packages/dock/test/human.test.ts`, and in
709
- Chromium behind `check:terrain`. Minimal on purpose: design, a
710
- switcher, the guest's page and the native app are left to the human
711
- document's own sitting, and none of them blocks a world from having
712
- a screen.
713
- 16. Removing: Done. `packages/quo/SPEC.md` gave the owner its fourth ask, `remove`,
714
- the mirror of knock: a relation out of a being by id, occupant or
715
- standing, and on the ward's own pk an owner, by the root alone. In
716
- `packages/quo/src/ward/owner.ts` with its test; the pilot's fourth tool; `quo
717
- remove` from the shell, the same word; the step 14 test asks it
718
- instead of reaching into the cells. The lab's leftovers went with it,
719
- recorded in `estates/lab/lab.md`.
720
- 17. What the lab wants: Done. A launchd agent beside the systemd unit in
721
- `estates/lab/droplet/`, the daemon as the user forever with
722
- `~/.quo/dial.json` naming the routes it holds a socket to, installed
723
- on the operator's Mac against the lab. And `quo run`: the runner from
724
- a terminal, the same pipe as `quo side` with a model named in the
725
- hello, the daemon running the runner beside the avatar, a line in a
726
- turn and the final text a line out, proven with a scripted endpoint
727
- as a child process in `packages/dock/test/cli.test.ts`.
728
- 18. The edge terrain: Done. `packages/dock/harbor/edge/`, the core over a Durable
729
- Object: its storage the store with the seed sealed under a platform
730
- secret, the deployed worker the code, the object the lease; reached by
731
- request, holding dialers' sockets, the root a route under a platform
732
- secret. The conformance suite passed untouched inside the platform's
733
- runtime, behind `check:terrain`; deployed to the lab's account and
734
- piloted from the Mac over a standing, `--via edge`. The third terrain
735
- the library move waited for; the `ws` question answered in the harbor
736
- document. `estates/lab/lab.md` keeps the record, one slip included.
737
-
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.
755
- 20. The dock published: Done. `@quo-systems/dock` 0.1.0, the same shape
756
- the library took, an emitted `dist/` with the `quo` command pointing
757
- into it, the source and the four documents beside it, and
758
- `packages/dock/test/package.test.ts` as the one suite that meets it as
759
- an adopter: the library's tarball and this one installed into an empty
760
- folder with nothing but Node, then `quo init`, a daemon, and
761
- `quo census` answering with the desk and the user being. The library
762
- went to 0.2.0 first, since the door's words moved the wire. What it
763
- decided: the daemon bundles the tab from beside itself, emitted or
764
- source, whichever is there, the one place the dock reads a file by
765
- path at run time.
766
-
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.
597
+ `next.md` is where the next sitting starts.
822
598
 
823
599
  ## Glossary
824
600
 
package/beings/user.ts CHANGED
@@ -12,19 +12,29 @@ import { Carrier } from './carry.ts';
12
12
  export const DESK = 'desk';
13
13
  const isDesk = (occ: OccupantRecord | undefined) => occ?.id === DESK;
14
14
  const isDevice = (occ: OccupantRecord | undefined) => occ !== undefined && occ.id !== DESK;
15
- const client = (occ: OccupantRecord | undefined) => (typeof occ?.notes.client === 'string' ? occ.notes.client : null);
16
- // A device the human allowed to wake her other devices: the note says so.
17
- const mayWake = (occ: OccupantRecord | undefined) => isDesk(occ) || occ?.notes.wake === true;
15
+ // A device is known by its client identity. The desk writes it as a note
16
+ // when it asks her to mint; the root, minting on her directly, writes
17
+ // nothing, and then the id the root chose is the identity.
18
+ const rootMinted = (occ: OccupantRecord | undefined) => isDevice(occ) && occ?.notes.client === undefined;
19
+ const client = (occ: OccupantRecord | undefined) => (typeof occ?.notes.client === 'string' ? occ.notes.client : rootMinted(occ) ? occ!.id : null);
20
+ // A device the human allowed to wake her other devices: the note says so,
21
+ // or the root minted it, and the root's word needs no note.
22
+ const mayWake = (occ: OccupantRecord | undefined) => isDesk(occ) || occ?.notes.wake === true || rootMinted(occ);
18
23
 
19
24
  // She carries her standings, acme, the calendar, the house, for a device the
20
25
  // human allowed to reach them at the exchange: the note says so. A device
21
26
  // without the note sees her own asks alone, and a model sees acme only
22
- // because the human said it may.
27
+ // because the human said it may. A device the root minted reaches: the root
28
+ // is the owner of everything she holds and chose to mint it.
23
29
  export class User extends Carrier {
24
30
  static override carries(occ: OccupantRecord | undefined): boolean {
25
- return isDevice(occ) && occ?.notes.reach === true;
31
+ return isDevice(occ) && (occ?.notes.reach === true || rootMinted(occ));
26
32
  }
27
- static override cells = { name: '', reports: [] as JsonObject[] };
33
+ // `ways` holds a device's way back that could not be taken when it was
34
+ // handed over: the knock was unreached, a tab whose pk the far harbor had
35
+ // not bound yet, a phone in a tunnel. Nothing delivered spends nothing,
36
+ // so the invitation is tried again at the next push.
37
+ static override cells = { name: '', reports: [] as JsonObject[], ways: {} as Record<string, Invitation> };
28
38
  static override asks = {
29
39
  hello: { description: 'say hello, and hand back an invitation so she can reach you', input: { type: 'object', properties: { invitation: { type: 'object' } } } },
30
40
  whoami: { description: 'who she thinks you are', input: { type: 'object' }, for: isDevice },
@@ -36,16 +46,30 @@ export class User extends Carrier {
36
46
  };
37
47
 
38
48
  // Anyone may say hello. A device that hands her an invitation in the args
39
- // is taken as a standing under its own id, so she can push to it later.
49
+ // is taken as a standing under its own id, so she can push to it later;
50
+ // if it cannot be taken now, it is kept and tried at the next push.
40
51
  async hello(args: JsonObject, asker: Asker) {
41
52
  if (asker.id !== undefined && args.invitation && typeof args.invitation === 'object' && !Array.isArray(args.invitation)) {
42
- const inv = args.invitation as Invitation;
43
- const back = await this.knock(inv);
44
- if (!isSilence(back) && !isWord(back)) await this.take(`to:${asker.id}`, inv);
53
+ await this.wayBack(asker.id, args.invitation as Invitation);
45
54
  }
46
55
  return { welcome: asker.id ?? null, name: this.cells.name };
47
56
  }
48
57
 
58
+ // Take a device's way back, or keep it for later when the knock did not
59
+ // reach. A refusal spends the heir and is final; the way is dropped.
60
+ async wayBack(id: string, inv: Invitation): Promise<boolean> {
61
+ const ways = this.cells.ways as Record<string, Invitation>;
62
+ const back = await this.knock(inv);
63
+ if (!isSilence(back) && !isWord(back)) {
64
+ await this.take(`to:${id}`, inv);
65
+ delete ways[id];
66
+ return true;
67
+ }
68
+ if (isWord(back) && wordOf(back) !== 'unreached') delete ways[id];
69
+ else ways[id] = inv;
70
+ return false;
71
+ }
72
+
49
73
  whoami(_args: JsonObject, asker: Asker) {
50
74
  return { id: asker.id ?? null, client: client(this.occupant(asker)) };
51
75
  }
@@ -74,7 +98,10 @@ export class User extends Carrier {
74
98
  // stops being able to ask her and keeps receiving everything she pushes.
75
99
  async push(args: JsonObject) {
76
100
  const c = typeof args.client === 'string' ? args.client : null;
77
- const st = c === null || !this.cells.occupants[c] ? undefined : this.standings[`to:${c}`];
101
+ if (c === null || !this.cells.occupants[c]) return { error: 'no such device, or it gave no way back' };
102
+ const kept = (this.cells.ways as Record<string, Invitation>)[c];
103
+ if (!this.standings[`to:${c}`] && kept) await this.wayBack(c, kept);
104
+ const st = this.standings[`to:${c}`];
78
105
  if (!st) return { error: 'no such device, or it gave no way back' };
79
106
  const out = await st.ask('notify', (args.object as JsonObject) ?? {});
80
107
  return isSilence(out) ? { error: 'silence' } : isWord(out) ? { error: wordOf(out) } : { pushed: out };
@@ -88,6 +115,7 @@ export class User extends Carrier {
88
115
  if (c === null) return { error: 'client is a string' };
89
116
  this.occupants.remove(c);
90
117
  this.standings.remove(`to:${c}`);
118
+ delete (this.cells.ways as Record<string, Invitation>)[c];
91
119
  return { forgot: c };
92
120
  }
93
121