@nervur-org/dock 0.4.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (225) hide show
  1. package/README.md +17 -15
  2. package/api/route.ts +4 -72
  3. package/beings/avatar.ts +11 -7
  4. package/beings/carry.ts +129 -116
  5. package/beings/clock.ts +11 -7
  6. package/beings/desk.ts +7 -6
  7. package/beings/doorbell.ts +36 -7
  8. package/beings/index.ts +17 -4
  9. package/beings/join.ts +7 -4
  10. package/beings/link.ts +1 -1
  11. package/beings/porter.ts +5 -1
  12. package/beings/presence.ts +15 -5
  13. package/beings/setup.ts +27 -21
  14. package/beings/user.ts +26 -17
  15. package/beings/way.ts +1 -1
  16. package/{dock → box}/agents.ts +37 -44
  17. package/{dock → box}/apns.ts +4 -4
  18. package/box/box.ts +31 -0
  19. package/box/contract.ts +41 -0
  20. package/box/dialers.ts +88 -0
  21. package/box/dock.ts +148 -0
  22. package/box/envoy.ts +96 -0
  23. package/box/faculty.ts +111 -0
  24. package/{dock → box}/fcm.ts +1 -1
  25. package/box/grants.ts +90 -0
  26. package/box/http.ts +93 -0
  27. package/{dock → box}/http2.ts +7 -7
  28. package/box/index.ts +201 -0
  29. package/box/join.ts +78 -0
  30. package/box/local.ts +37 -0
  31. package/{dock → box}/location.ts +15 -3
  32. package/{dock → box}/notifications.ts +17 -5
  33. package/box/push.ts +85 -0
  34. package/box/ring.ts +85 -0
  35. package/{dock → box}/service.ts +2 -2
  36. package/box/timer.ts +150 -0
  37. package/box/tree.ts +22 -0
  38. package/{dock → box}/webpush.ts +2 -2
  39. package/cli/client.ts +1 -1
  40. package/cli/daemon.ts +101 -103
  41. package/cli/estate.ts +2 -3
  42. package/cli/nervur.ts +44 -25
  43. package/cli/stand.ts +1 -1
  44. package/dist/api/route.d.ts +1 -2
  45. package/dist/api/route.js +4 -76
  46. package/dist/beings/avatar.d.ts +4 -3
  47. package/dist/beings/avatar.js +9 -6
  48. package/dist/beings/carry.d.ts +10 -12
  49. package/dist/beings/carry.js +119 -127
  50. package/dist/beings/clock.js +10 -7
  51. package/dist/beings/desk.js +8 -7
  52. package/dist/beings/doorbell.d.ts +3 -1
  53. package/dist/beings/doorbell.js +37 -4
  54. package/dist/beings/index.d.ts +24 -4
  55. package/dist/beings/index.js +15 -4
  56. package/dist/beings/join.d.ts +1 -0
  57. package/dist/beings/join.js +6 -4
  58. package/dist/beings/link.d.ts +0 -1
  59. package/dist/beings/link.js +1 -1
  60. package/dist/beings/porter.js +5 -1
  61. package/dist/beings/presence.d.ts +1 -0
  62. package/dist/beings/presence.js +12 -3
  63. package/dist/beings/setup.d.ts +5 -2
  64. package/dist/beings/setup.js +16 -15
  65. package/dist/beings/user.d.ts +4 -3
  66. package/dist/beings/user.js +18 -11
  67. package/dist/{dock → box}/agents.d.ts +7 -4
  68. package/dist/{dock → box}/agents.js +30 -37
  69. package/dist/{dock → box}/apns.d.ts +1 -1
  70. package/dist/{dock → box}/apns.js +3 -3
  71. package/dist/box/box.d.ts +9 -0
  72. package/dist/box/box.js +18 -0
  73. package/dist/box/contract.d.ts +6 -0
  74. package/dist/box/contract.js +35 -0
  75. package/dist/{dock → box}/dialers.d.ts +7 -4
  76. package/dist/box/dialers.js +76 -0
  77. package/dist/box/dock.d.ts +182 -0
  78. package/dist/box/dock.js +128 -0
  79. package/dist/box/envoy.d.ts +44 -0
  80. package/dist/box/envoy.js +75 -0
  81. package/dist/box/faculty.d.ts +68 -0
  82. package/dist/box/faculty.js +97 -0
  83. package/dist/{dock → box}/fcm.d.ts +1 -1
  84. package/dist/{dock → box}/grants.d.ts +4 -3
  85. package/dist/box/grants.js +78 -0
  86. package/dist/box/http.d.ts +26 -0
  87. package/dist/box/http.js +89 -0
  88. package/dist/{dock → box}/http2.d.ts +1 -1
  89. package/dist/{dock → box}/http2.js +6 -6
  90. package/dist/box/index.d.ts +46 -0
  91. package/dist/box/index.js +179 -0
  92. package/dist/box/join.d.ts +19 -0
  93. package/dist/box/join.js +67 -0
  94. package/dist/{dock → box}/local.d.ts +3 -2
  95. package/dist/box/local.js +27 -0
  96. package/dist/{dock → box}/location.d.ts +3 -0
  97. package/dist/box/location.js +21 -0
  98. package/dist/{dock → box}/notifications.d.ts +3 -0
  99. package/dist/{dock → box}/notifications.js +14 -4
  100. package/dist/{dock → box}/push.d.ts +12 -18
  101. package/dist/box/push.js +83 -0
  102. package/dist/box/ring.d.ts +18 -0
  103. package/dist/box/ring.js +55 -0
  104. package/dist/{dock → box}/service.d.ts +1 -1
  105. package/dist/{dock → box}/service.js +1 -1
  106. package/dist/{dock → box}/timer.d.ts +2 -0
  107. package/dist/box/timer.js +127 -0
  108. package/dist/box/tree.d.ts +5 -0
  109. package/dist/box/tree.js +14 -0
  110. package/dist/{dock → box}/webpush.d.ts +1 -1
  111. package/dist/cli/client.d.ts +1 -0
  112. package/dist/cli/daemon.d.ts +2 -8
  113. package/dist/cli/daemon.js +102 -107
  114. package/dist/cli/estate.js +2 -3
  115. package/dist/cli/nervur.js +44 -24
  116. package/dist/cli/stand.d.ts +0 -14
  117. package/dist/cli/stand.js +1 -1
  118. package/dist/harbor/browser.d.ts +8 -5
  119. package/dist/harbor/browser.js +20 -21
  120. package/dist/harbor/capacitor.d.ts +3 -3
  121. package/dist/harbor/capacitor.js +9 -8
  122. package/dist/harbor/disk.d.ts +9 -9
  123. package/dist/harbor/disk.js +56 -51
  124. package/dist/harbor/edge/alarm.d.ts +4 -2
  125. package/dist/harbor/edge/alarm.js +9 -19
  126. package/dist/harbor/edge/edge.d.ts +7 -6
  127. package/dist/harbor/edge/edge.js +58 -53
  128. package/dist/harbor/edge/exercise.js +4 -4
  129. package/dist/harbor/edge/given.d.ts +0 -1
  130. package/dist/harbor/edge/given.js +0 -29
  131. package/dist/harbor/edge/storage.d.ts +0 -2
  132. package/dist/harbor/edge/storage.js +2 -2
  133. package/dist/harbor/http.js +4 -2
  134. package/dist/harbor/sealed.d.ts +0 -5
  135. package/dist/harbor/sealed.js +0 -39
  136. package/dist/harbor/tauri.d.ts +2 -2
  137. package/dist/harbor/tauri.js +13 -10
  138. package/dist/human/app.d.ts +1 -1
  139. package/dist/human/app.js +10 -12
  140. package/dist/human/door.d.ts +0 -1
  141. package/dist/human/door.js +1 -1
  142. package/dist/human/html.js +1 -1
  143. package/dist/human/move.js +1 -1
  144. package/dist/human/tab.bundle.txt +3629 -2824
  145. package/dist/human/wake.d.ts +5 -2
  146. package/dist/human/wake.js +23 -14
  147. package/dist/human/web.d.ts +0 -1
  148. package/dist/human/web.js +3 -3
  149. package/dist/human/worker.bundle.txt +2945 -2118
  150. package/dist/human/worlds.js +3 -3
  151. package/dist/mcp/agent.d.ts +0 -1
  152. package/dist/mcp/agent.js +1 -1
  153. package/dist/mcp/allow.js +3 -2
  154. package/dist/mcp/http.d.ts +0 -1
  155. package/dist/mcp/http.js +1 -1
  156. package/dist/mcp/oauth.d.ts +3 -4
  157. package/dist/mcp/oauth.js +4 -4
  158. package/dist/mcp/route.d.ts +1 -1
  159. package/dist/mcp/runner.d.ts +0 -1
  160. package/dist/mcp/runner.js +1 -1
  161. package/dist/mcp/server.d.ts +0 -1
  162. package/dist/mcp/server.js +1 -1
  163. package/harbor/browser.ts +22 -24
  164. package/harbor/capacitor.ts +9 -8
  165. package/harbor/disk.ts +53 -48
  166. package/harbor/edge/alarm.ts +15 -12
  167. package/harbor/edge/edge.ts +61 -55
  168. package/harbor/edge/exercise.ts +4 -4
  169. package/harbor/edge/given.ts +3 -33
  170. package/harbor/edge/storage.ts +2 -2
  171. package/harbor/http.ts +5 -3
  172. package/harbor/sealed.ts +0 -35
  173. package/harbor/tauri.ts +13 -10
  174. package/human/app.ts +12 -13
  175. package/human/door.ts +1 -1
  176. package/human/html.ts +1 -1
  177. package/human/local.ts +1 -1
  178. package/human/move.ts +1 -1
  179. package/human/tab.bundle.txt +3629 -2824
  180. package/human/wake.ts +36 -16
  181. package/human/web.ts +4 -4
  182. package/human/worker.bundle.txt +2945 -2118
  183. package/human/worlds.ts +3 -3
  184. package/mcp/agent.ts +1 -1
  185. package/mcp/allow.ts +2 -1
  186. package/mcp/http.ts +1 -1
  187. package/mcp/oauth.ts +4 -4
  188. package/mcp/route.ts +1 -1
  189. package/mcp/runner.ts +1 -1
  190. package/mcp/server.ts +1 -1
  191. package/package.json +10 -18
  192. package/GETTING_STARTED.md +0 -138
  193. package/GLOSSARY.md +0 -118
  194. package/WORLDS.md +0 -569
  195. package/beings/envoy.ts +0 -218
  196. package/dist/beings/envoy.d.ts +0 -54
  197. package/dist/beings/envoy.js +0 -211
  198. package/dist/dock/console.d.ts +0 -6
  199. package/dist/dock/console.js +0 -20
  200. package/dist/dock/dialers.js +0 -75
  201. package/dist/dock/faculty.d.ts +0 -24
  202. package/dist/dock/faculty.js +0 -142
  203. package/dist/dock/grants.js +0 -99
  204. package/dist/dock/index.d.ts +0 -41
  205. package/dist/dock/index.js +0 -113
  206. package/dist/dock/local.js +0 -44
  207. package/dist/dock/location.js +0 -11
  208. package/dist/dock/push.js +0 -132
  209. package/dist/dock/timer.js +0 -149
  210. package/dock/console.ts +0 -23
  211. package/dock/dialers.ts +0 -85
  212. package/dock/faculty.ts +0 -148
  213. package/dock/grants.ts +0 -111
  214. package/dock/index.ts +0 -133
  215. package/dock/local.ts +0 -48
  216. package/dock/push.ts +0 -161
  217. package/dock/timer.ts +0 -174
  218. package/quo-api.md +0 -196
  219. package/quo-app-bells.md +0 -280
  220. package/quo-dock.md +0 -1945
  221. package/quo-harbor.md +0 -874
  222. package/quo-human.md +0 -1042
  223. package/quo-mcp.md +0 -343
  224. /package/dist/{dock → box}/fcm.js +0 -0
  225. /package/dist/{dock → box}/webpush.js +0 -0
@@ -22,7 +22,7 @@
22
22
  // invitation to this doorbell, so a bell that held both halves of the
23
23
  // relation is a bell that could read a banner, and this one cannot.
24
24
  import { Being, isSilence, isWord, wordOf } from 'nervur';
25
- import type { Asker, JsonObject, OccupantRecord } from 'nervur';
25
+ import type { Asker, JsonObject, OccupantRecord, Standing } from 'nervur';
26
26
 
27
27
  const DEVICE = 'device';
28
28
  // Her standing at the box's push faculty, under an id of her own. The ward
@@ -30,13 +30,19 @@ const DEVICE = 'device';
30
30
  const MINE = 'push';
31
31
  // What a platform will carry. It is the faculty's number, because the
32
32
  // faculty is the one that hands a ring to a platform.
33
- import { SEAL } from '../dock/push.ts';
33
+ import { SEAL, isToken } from '../box/ring.ts';
34
+ import { PUSH } from '../box/push.ts';
35
+ import { join } from '../box/join.ts';
34
36
 
35
37
  const isDevice = (occ: OccupantRecord | undefined) => occ?.id === DEVICE;
36
38
  const isWorld = (occ: OccupantRecord | undefined) => occ !== undefined && occ.id !== DEVICE;
37
39
  const isMaker = (occ: OccupantRecord | undefined) => occ !== undefined;
38
40
 
39
41
  export class Doorbell extends Being {
42
+ // Where this class lives in a tree, which is her key in a ward. She says
43
+ // it herself, so the porter who boots her spells no path of her own and a
44
+ // class moved is one line moved with it.
45
+ static readonly at = 'beings/Doorbell';
40
46
  static override cells: JsonObject = { token: null, admitted: 0, claimed: false };
41
47
  static override asks = {
42
48
  claim: { description: 'the one way in for this doorbell, handed out once', input: { type: 'object' }, for: isMaker },
@@ -73,11 +79,34 @@ export class Doorbell extends Being {
73
79
 
74
80
  // Written once and rewritten whenever the platform hands the phone a new
75
81
  // one. No world is told anything, because no world was ever given it.
76
- keep(args: JsonObject): JsonObject {
82
+ //
83
+ // A kind is one of the rings the box behind this bell holds, and a token
84
+ // of any other kind is refused here, at the door, with an error object.
85
+ // The faculty is asked which kinds it rings, because the faculty is the
86
+ // one that hands a ring to a platform and nothing else on this bell knows.
87
+ // A device told at the moment it hands a token can hand another or ask its
88
+ // human for a bell that can reach it; a device told nothing goes on
89
+ // believing it is reachable until a world rings and is refused, and by then
90
+ // the only one who could act is the only one not there.
91
+ async keep(args: JsonObject): Promise<JsonObject> {
77
92
  const token = args.token;
78
- if (token === null || typeof token !== 'object' || Array.isArray(token) || typeof (token as { kind?: unknown }).kind !== 'string') return { error: 'a token names its kind' };
93
+ if (!isToken(token)) return { error: 'a token names its kind' };
94
+ const at = await this.at();
95
+ if (!at) return { error: 'this box rings nothing' };
96
+ const said = await at.ask('kinds', {});
97
+ if (said === undefined || isSilence(said)) return { error: 'this box rings nothing' };
98
+ if (isWord(said)) return { error: wordOf(said) };
99
+ const kinds = (said as { kinds?: unknown }).kinds;
100
+ if (!Array.isArray(kinds)) return { error: 'this box rings nothing' };
101
+ if (!kinds.includes(token.kind)) return { error: `this box cannot ring a ${token.kind}` };
79
102
  this.cells.token = token;
80
- return { kept: (token as { kind: string }).kind };
103
+ return { kept: token.kind };
104
+ }
105
+
106
+ // Her standing at the box's push faculty, taken the first time it is
107
+ // wanted and held from then on.
108
+ private async at(): Promise<Standing | undefined> {
109
+ return this.standings[MINE] ?? (await join(this, PUSH, MINE).then(() => this.standings[MINE]));
81
110
  }
82
111
 
83
112
  // The device allows a world by asking for a way in and handing it over.
@@ -113,7 +142,7 @@ export class Doorbell extends Being {
113
142
  if (sealed.length > SEAL) return { error: `a sealed payload is at most ${SEAL} bytes` };
114
143
  const token = this.cells.token;
115
144
  if (token === null) return { error: 'this device has handed no token' };
116
- const at = this.standings[MINE] ?? (await this.lend(MINE, MINE).then(() => this.standings[MINE]));
145
+ const at = await this.at();
117
146
  if (!at) return { error: 'this box rings nothing' };
118
147
  const out = await at.ask('ring', { token, sealed });
119
148
  if (out === undefined || isSilence(out)) return { error: 'this box rings nothing' };
@@ -123,7 +152,7 @@ export class Doorbell extends Being {
123
152
  // not take the ring. Passing that on is the whole of the promise a bell
124
153
  // makes, because a ring reported as rung and never sent is worse than one
125
154
  // that plainly failed: the world stops trying and the human is never told.
126
- if (out !== null && typeof out === 'object' && !Array.isArray(out) && typeof (out as JsonObject).error === 'string') return { error: (out as JsonObject).error as string };
155
+ if (out !== null && typeof out === 'object' && !Array.isArray(out) && typeof out.error === 'string') return { error: out.error };
127
156
  return { rang: true };
128
157
  }
129
158
  }
package/beings/index.ts CHANGED
@@ -4,13 +4,26 @@ export { User, DESK, HOME, BELL } from './user.ts';
4
4
  export { Desk } from './desk.ts';
5
5
  export { WAY, kindOf, verified, VERIFY, SEND, type Proof, type Verified } from './way.ts';
6
6
  export { Avatar, USER, PUSHER } from './avatar.ts';
7
- export { Carrier } from './carry.ts';
7
+ export { carry, blueprint, CARRIES, type Carrying } from './carry.ts';
8
8
  export { Presence } from './presence.ts';
9
9
  export { Courier, MOVER, isMoving, type Moving } from './courier.ts';
10
10
  export { Porter } from './porter.ts';
11
11
  export { Doorbell } from './doorbell.ts';
12
- export { Envoy, owner, occupant, type Delivery, type Wake, type Heard } from './envoy.ts';
13
- export { DOCK, CONSOLE, LOCAL, LOCATION, NOTIFICATIONS, PUSH, TIMER, FACULTIES, FACULTY_CLASSES, Console, Faculty, Local, Location, Notifications, Push, Timer, isToken, join, settled, stand, standOn, lending, facultyOf, type Ring, type Token, type Where, type Watched, type Woke, type Rooted } from '../dock/index.ts';
14
- export { webPush, mintVapid, publicOf, apnsPush, pkcs8, HOSTS, fcmPush, isAccount, DOORS, handedTo, ringing, routed, type Vapid, type Apns, type Account, type Doors } from '../dock/index.ts';
12
+ export { BOX, BOX_SEED, Dock, LOCAL, LOCATION, NOTIFICATIONS, PUSH, TIMER, Faculty, Local, Location, Notifications, Push, Timer, contract, covers, nameOf, titleOf, keyed, join, settled, stand, standHarbor, classes, lent, type Contract, type Tree, type Where, type Watched, type Rooted } from '../box/index.ts';
13
+ export { isToken, type Ring, type Token } from '../box/index.ts';
14
+ export { webPush, mintVapid, publicOf, apnsPush, pkcs8, HOSTS, fcmPush, isAccount, DOORS, handedTo, ringing, routed, type Vapid, type Apns, type Account, type Doors } from '../box/index.ts';
15
15
  export { Clock, CLOCK, setupClock, placeClock, readSchedule, parseCron, nextFire, civil, fromCivil, type Schedule, type Fire, type Cron, type Civil } from './clock.ts';
16
16
  export { sanitise, hint, hintFor, groups, type Look, type Hint } from './look.ts';
17
+
18
+ import { User } from './user.ts';
19
+ import { Desk } from './desk.ts';
20
+ import { Avatar } from './avatar.ts';
21
+ import { Presence } from './presence.ts';
22
+ import { Clock } from './clock.ts';
23
+ import { Courier } from './courier.ts';
24
+ import { Porter } from './porter.ts';
25
+ import { Doorbell } from './doorbell.ts';
26
+
27
+ // The beings this folder brings to every harbor, as a tree: a ward names
28
+ // one by where it lives, `beings/User`, as it names a faculty.
29
+ export const BEINGS = { beings: { User, Desk, Avatar, Presence, Clock, Courier, Porter, Doorbell } };
package/beings/join.ts CHANGED
@@ -11,14 +11,17 @@ import type { Hosted } from 'nervur/harbor';
11
11
  import type { Invitation } from 'nervur';
12
12
  import type { Avatar } from './avatar.ts';
13
13
 
14
- // Her avatar in a ward, found or booted under the identity's key. An
15
- // identity is a word, and never a being of the ward.
14
+ // An identity is a word, and never the key of a being the ward holds: the
15
+ // ward is asked, so no route names who its beings are.
16
+ export const isIdentity = (hosted: Hosted, identity: unknown): identity is string => typeof identity === 'string' && /^[\w.-]+$/.test(identity) && !hosted.keys().includes(identity);
17
+
18
+ // Her avatar in a ward, found or booted under the identity's key.
16
19
  export async function avatarOf(hosted: Hosted, identity: string): Promise<{ avatar?: Avatar; error?: string }> {
17
- if (!/^[\w.-]+$/.test(identity) || identity === hosted.record.user || identity === 'desk') return { error: 'an identity is a word, and not a being of the ward' };
20
+ if (!isIdentity(hosted, identity)) return { error: 'an identity is a word, and not a being of the ward' };
18
21
  const key = `avatar:${identity}`;
19
22
  let avatar = hosted.being(key) as Avatar | undefined;
20
23
  if (!avatar) {
21
- const out = (await hosted.ask('boot', { key, class: 'Avatar' })) as { error?: string };
24
+ const out = (await hosted.ask('boot', { key, class: 'beings/Avatar' })) as { error?: string };
22
25
  if (out.error) return { error: out.error };
23
26
  avatar = hosted.being(key) as Avatar;
24
27
  }
package/beings/link.ts CHANGED
@@ -17,7 +17,7 @@ export const KEY = 'nervur';
17
17
  // a query string is in every log and every referer, and one read of it is
18
18
  // one way in taken. `<kind>.<token>`, the kind the desk knows it by and the
19
19
  // token that door minted.
20
- export const PROOF = 'proof';
20
+ const PROOF = 'proof';
21
21
  const HEX = (n: number) => `[0-9a-f]{${n}}`;
22
22
  const FULL = new RegExp(`^(${HEX(128)})\\.(${HEX(64)})\\.(${HEX(64)})$`);
23
23
  const PUBLIC = new RegExp(`^${HEX(128)}$`);
package/beings/porter.ts CHANGED
@@ -15,6 +15,7 @@
15
15
  // that a loop in this session stops somewhere a human can see.
16
16
  import { Being, isSilence, isWord, wordOf } from 'nervur';
17
17
  import type { JsonObject } from 'nervur';
18
+ import { Doorbell } from './doorbell.ts';
18
19
 
19
20
  const ROOM = 64;
20
21
 
@@ -28,8 +29,11 @@ export class Porter extends Being {
28
29
  const minted = typeof this.cells.minted === 'number' ? this.cells.minted : 0;
29
30
  if (minted >= ROOM) return { error: 'this bell holds no more doorbells' };
30
31
 
32
+ // The class says where it lives and the porter reads it off her. A path
33
+ // spelled here would be the doorbell named by whoever boots her, which
34
+ // is a class described by someone who is not it.
31
35
  const key = `doorbell:${minted + 1}`;
32
- const held = await this.boot('Doorbell', key, key);
36
+ const held = await this.boot(Doorbell.at, key, key);
33
37
  if (held === null) return { error: 'this bell could not make a doorbell' };
34
38
  this.cells.minted = minted + 1;
35
39
 
@@ -15,25 +15,35 @@
15
15
  // `settled` before it says the box is up.
16
16
  import type { Asker, OccupantRecord, Stance } from 'nervur';
17
17
  import { Avatar, PUSHER, USER } from './avatar.ts';
18
- import { FACULTIES, join } from '../dock/index.ts';
18
+ import { LOCATION } from '../box/location.ts';
19
+ import { NOTIFICATIONS } from '../box/notifications.ts';
20
+ import { PUSH } from '../box/push.ts';
21
+ import { TIMER } from '../box/timer.ts';
22
+ import { join } from '../box/join.ts';
23
+ import { titleOf } from '../box/contract.ts';
19
24
 
20
25
  export class Presence extends Avatar {
21
26
  // Her world's user being sees the box, and nobody else: the way back she
22
27
  // handed over at the join is the one occupant who may ask through her.
23
- static override carries(occupant: OccupantRecord | undefined, _asker: Asker): boolean {
28
+ static carries(occupant: OccupantRecord | undefined, _asker: Asker): boolean {
24
29
  return occupant?.id === PUSHER;
25
30
  }
26
31
  // Her standing at the user being is hers to speak through and never to
27
32
  // carry: the user being carries her, so carrying it back would be a
28
33
  // loop, and it is the one standing that is not a faculty.
29
- static override skips(id: string): boolean {
34
+ static skips(id: string): boolean {
30
35
  return id === USER;
31
36
  }
32
37
 
33
- // Which names the box lent her, by name, settled after birth.
38
+ // What a device can do that a world may want: the contracts she asks her
39
+ // box for, each held under the word the contract calls itself.
40
+ static needs = [LOCATION, NOTIFICATIONS, PUSH, TIMER];
41
+
42
+ // Which of them the box lent her, by that word, settled after birth.
34
43
  readonly lent: Promise<Record<string, string | null>>;
35
44
  constructor(stance: Stance) {
36
45
  super(stance);
37
- this.lent = Promise.all(Object.keys(FACULTIES).map(async (name) => [name, await join(this, name)] as const)).then((all) => Object.fromEntries(all));
46
+ const C = this.constructor as typeof Presence;
47
+ this.lent = Promise.all(C.needs.map(async (c) => [titleOf(c), await join(this, c, titleOf(c))] as const)).then((all) => Object.fromEntries(all));
38
48
  }
39
49
  }
package/beings/setup.ts CHANGED
@@ -1,45 +1,51 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
- // The root's setup of a fresh ward, as every terrain does it: the front desk
3
- // booted public, one user being, the desk holding a standing to her under
4
- // `user:<name>`, placed by the root's knock, and she holding her own ward's
5
- // door as an owner under `home`. She names the desk `desk`,
6
- // and that id is the only thing that makes it the desk. A disk harbor does
7
- // this at `nervur init`, the edge at its root route's init; the memory
8
- // harbor's test world does the same by hand.
2
+ // The root's setup of a fresh ward, as every terrain does it: the front
3
+ // booted public under `desk`, one home being, the front holding a standing
4
+ // to her under `user:<name>`, placed by the root's knock, and she holding her
5
+ // own ward's door as an owner under `home`. She names the front `desk`, and
6
+ // that id is the only thing that makes it the front. A disk harbor does this
7
+ // at `nervur init`, the edge at its root route's init; the memory harbor's
8
+ // test world does the same by hand.
9
9
  import type { Invitation } from 'nervur';
10
10
  import type { Hosted } from 'nervur/harbor';
11
11
  import { DESK, HOME } from './user.ts';
12
12
 
13
+ // The classes setup boots, named where they live. A terrain hands its own:
14
+ // an organisation's ward has an org's home being of the estate's class, and
15
+ // a ward may have an estate's front in place of the dock's.
16
+ export type Setup = { front: string; home: string };
17
+ const SETUP: Setup ={ front: 'beings/Desk', home: 'beings/User' };
18
+
13
19
  // The root boots and steps back: it invites an owner on the ward's own pk
14
- // and the user being knocks for it, so from then on placing a being,
20
+ // and the home being knocks for it, so from then on placing a being,
15
21
  // minting a way in or removing one is an ask on her, shown by her gate to a
16
22
  // device that may mint, and the box is not logged into for it. The same
17
23
  // standing a user being of another ward takes to own this one; here it is
18
24
  // her own.
19
- export async function setupHome(hosted: Hosted, user: string): Promise<void> {
25
+ async function setupHome(hosted: Hosted, user: string): Promise<void> {
20
26
  const inv = (await hosted.ask('invite', { being: hosted.pk, id: HOME })) as Invitation | { error?: string };
21
27
  if ('error' in inv) throw new Error(`home: ${inv.error}`);
22
28
  const placed = (await hosted.ask('knock', { being: user, id: HOME, invitation: inv })) as { taken?: string; error?: string };
23
29
  if (placed.taken !== HOME) throw new Error(`home: ${placed.error ?? 'she could not take her own door'}`);
24
30
  }
25
31
 
26
- // The ward's home being is a `User` unless the device says which class: an
27
- // organisation's ward has an org's home being, of the estate's own class,
28
- // and everything else of setup, the desk and the door, is placed on her the
29
- // same way.
30
- export async function setup(hosted: Hosted, user: string, cls = 'User'): Promise<void> {
31
- if (!/^[\w.-]+$/.test(user) || user === 'desk') throw new Error(`a user is a word, and not desk: ${user}`);
32
+ // Everything setup places is placed the same way whatever the classes are:
33
+ // a home key the front already holds is refused by the ward, as any key
34
+ // taken is.
35
+ export async function setup(hosted: Hosted, user: string, classes: Partial<Setup> = {}): Promise<void> {
36
+ const { front, home } = { ...SETUP, ...classes };
37
+ if (!/^[\w.-]+$/.test(user)) throw new Error(`a user is a word: ${user}`);
32
38
  const boot = async (args: Record<string, unknown>) => {
33
39
  const out = (await hosted.ask('boot', args)) as { booted?: string; error?: string };
34
40
  if (out.error) throw new Error(`init: ${out.error}`);
35
41
  };
36
- await boot({ key: 'desk', class: 'Desk' });
37
- const pub = (await hosted.ask('public', { key: 'desk' })) as { error?: string };
42
+ await boot({ key: DESK, class: front });
43
+ const pub = (await hosted.ask('public', { key: DESK })) as { error?: string };
38
44
  if (pub.error) throw new Error(`init: ${pub.error}`);
39
- await boot({ key: user, class: cls });
45
+ await boot({ key: user, class: home });
40
46
  const inv = (await hosted.ask('invite', { being: user, id: DESK })) as Invitation;
41
- // the desk's first hello names her, since a being does not know her own key
42
- const placed = (await hosted.ask('knock', { being: 'desk', id: `user:${user}`, invitation: inv, method: 'hello', args: { name: user } })) as { taken?: string };
43
- if (placed.taken !== `user:${user}`) throw new Error('init: the desk could not reach the user being');
47
+ // the front's first hello names her, since a being does not know her own key
48
+ const placed = (await hosted.ask('knock', { being: DESK, id: `user:${user}`, invitation: inv, method: 'hello', args: { name: user } })) as { taken?: string };
49
+ if (placed.taken !== `user:${user}`) throw new Error('init: the front could not reach the home being');
44
50
  await setupHome(hosted, user);
45
51
  }
package/beings/user.ts CHANGED
@@ -7,10 +7,15 @@
7
7
  // client identity the desk established. A device she let mint brings the
8
8
  // next one in, so a human adds a device from a device already in and only
9
9
  // the first one has to come from the root, on the box.
10
- import { isInvitation, isSilence, isWord, wordOf } from 'nervur';
11
- import type { Asker, Blueprint, Invitation, JsonObject, OccupantRecord, Stance } from 'nervur';
12
- import { Carrier } from './carry.ts';
13
- import { PUSH, banner, isToken, join, sealBanner } from '../dock/index.ts';
10
+ import { Being, isInvitation, isSilence, isWord, wordOf } from 'nervur';
11
+ import type { Asker, Blueprint, Invitation, JsonObject, OccupantRecord, Reply, Stance } from 'nervur';
12
+ import { CARRIES, carry } from './carry.ts';
13
+ import { banner, isToken, sealBanner } from '../box/index.ts';
14
+ import { PUSH as PUSHING } from '../box/push.ts';
15
+ import { join } from '../box/join.ts';
16
+
17
+ // Her standing at the box's push faculty, under an id of her own.
18
+ const PUSH = 'push';
14
19
 
15
20
  export const DESK = 'desk';
16
21
  // The prefix her doorbell standings are filed under, one per device: the
@@ -68,13 +73,13 @@ const mayMint = (occ: OccupantRecord | undefined) => isDesk(occ) || (isDevice(oc
68
73
  // is a choice an org makes rather than one the dock makes for it.
69
74
  const mayRevoke = (occ: OccupantRecord | undefined) => isDesk(occ) || (isDevice(occ) && held(occ).revoke);
70
75
 
71
- // She carries her standings, acme, the calendar, the house, for a device the
72
- // human allowed to reach them at the exchange: the note says so. A device
73
- // without the note sees her own asks alone, and a model sees acme only
76
+ // She carries her standings, her org, the calendar, the house, for a device
77
+ // the human allowed to reach them at the exchange: the note says so. A device
78
+ // without the note sees her own asks alone, and a model sees the org only
74
79
  // because the human said it may. A device the root minted reaches: the root
75
80
  // is the owner of everything she holds and chose to mint it.
76
- export class User extends Carrier {
77
- static override carries(occ: OccupantRecord | undefined): boolean {
81
+ export class User extends Being {
82
+ static carries(occ: OccupantRecord | undefined): boolean {
78
83
  return isDevice(occ) && held(occ).reach;
79
84
  }
80
85
  // `ways` holds a device's way back that could not be taken when it was
@@ -82,20 +87,24 @@ export class User extends Carrier {
82
87
  // not bound yet, a phone in a tunnel. Nothing delivered spends nothing,
83
88
  // so the invitation is tried again at the next push.
84
89
  // The bell standing is plumbing and carries nothing: a device that may
85
- // reach what she holds sees acme and the house, and never the envoy that
90
+ // reach what she holds sees the org and the house, and never the envoy that
86
91
  // wakes it. Her ways back are plumbing too, and a way back to a presence
87
92
  // being carries that device's faculties, `to:phone-location-where`, which
88
93
  // is how a model at home learns where the human is.
89
- static override hidden(id: string): boolean {
90
- return super.hidden(id) || id === PUSH || id.startsWith(`${BELL}:`);
94
+ static hidden(id: string): boolean {
95
+ return CARRIES.hidden(id) || id === PUSH || id.startsWith(`${BELL}:`);
91
96
  }
92
97
  // Her own door is carried to a device that may mint, and to nobody else:
93
98
  // `mint` is the word for a device trusted to change who is in her world,
94
99
  // and owning the world from that device is that trust said in full. A
95
- // device that reaches sees acme and the house, and no `home-boot`.
96
- static override shows(id: string, occ: OccupantRecord | undefined): boolean {
100
+ // device that reaches sees the org and the house, and no `home-boot`.
101
+ static shows(id: string, occ: OccupantRecord | undefined): boolean {
97
102
  return id !== HOME || held(occ).mint;
98
103
  }
104
+
105
+ override answer(asker: Asker, method?: string, args: JsonObject = {}): Promise<Reply> {
106
+ return carry(this, asker, method, args, (a, m, g) => super.answer(a, m, g));
107
+ }
99
108
  static override cells = { name: '', reports: [] as JsonObject[], ways: {} as Record<string, Invitation>, outbox: {} as Record<string, JsonObject[]> };
100
109
 
101
110
  // Her standing at the box's push faculty, taken again at every birth: a
@@ -111,7 +120,7 @@ export class User extends Carrier {
111
120
  readonly #push: Promise<string | null>;
112
121
  constructor(stance: Stance) {
113
122
  super(stance);
114
- this.#push = join(this, PUSH);
123
+ this.#push = join(this, PUSHING, PUSH);
115
124
  }
116
125
 
117
126
  // A world whose own being she is, is a home: a device reaching her holds
@@ -373,9 +382,9 @@ export class User extends Carrier {
373
382
  return Object.entries(this.cells.occupants)
374
383
  .filter(([id]) => id !== DESK)
375
384
  .map(([id, rec]) => {
376
- const notes = (rec as OccupantRecord).notes;
385
+ const notes = rec.notes;
377
386
  const name = typeof notes.client === 'string' ? notes.client : id;
378
- const has = MINTINGS.filter((w) => held(rec as OccupantRecord)[w]);
387
+ const has = MINTINGS.filter((w) => held(rec)[w]);
379
388
  const line = [...(name === me ? ['this one'] : []), ...(has.length ? [`may ${has.join(', ')}`] : [])].join(' · ');
380
389
  return { name, line, client: name, ...(name === me ? { you: true } : {}), ...(has.length ? { may: has } : {}) };
381
390
  });
package/beings/way.ts CHANGED
@@ -7,7 +7,7 @@
7
7
  //
8
8
  // Two kinds of holder, and both are ordinary beings:
9
9
  //
10
- // - the box's, lent by the dock ward, as `local` is: the daemon vouches for
10
+ // - the box's, lent by the box ward, as `local` is: the daemon vouches for
11
11
  // a process it saw on its own socket, and what it knows lives in that
12
12
  // being, on the box, dying with it.
13
13
  // - the world's own, in the world's ward, as an estate's post is: she holds
@@ -1,47 +1,30 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
- // The box's agents: `agents`, one faculty of the dock ward, standing for the
3
- // one thing a box does on an event as the timer does on a moment: run a
4
- // process. What she holds is the table of agents, in her cells and in no
5
- // file, placed by the root with `keep` the way a schedule is placed on the
6
- // clock, so a box has one place its state is and the daemon reads no recipe.
2
+ // The box's agents: on an event, run something for an identity, as the timer
3
+ // does on a moment. What she holds is the table of agents in her cells,
4
+ // placed by the root with `keep`. A row names a client identity, the ward
5
+ // she is admitted into, and what runs for her: a process, a command with its
6
+ // args in its folder, or a model at a url.
7
7
  //
8
- // A row names a client identity, the ward she is admitted into, and what
9
- // runs for her, one of two: a process, a command with its args in its
10
- // folder, or a model that speaks function calling, a base URL and a model
11
- // name, OpenRouter, LM Studio, anything with a tools array. At her birth
12
- // on a daemon she admits each identity into its ward as an avatar, through
13
- // the box's own way in, and stands the side her row says over that avatar.
14
- // A push from the user being is the event either way: the agent side runs
15
- // the command with it and reports what it printed; the runner side turns it
16
- // into a turn of the model's conversation with her describe as tools. The
17
- // queue is in the avatar's cells, so it survives a restart; the row is in
18
- // hers, so the agent comes back with the box.
19
- //
20
- // The daemon hands her how to run a row, as it hands the grants faculty how
21
- // to persist: running an agent is admitting through a socket only a daemon
22
- // has, and this file is not in the dock's index, since a tab has no process
23
- // to spawn. A box with no runner keeps the table and runs nobody.
8
+ // How a row runs is the terrain's, since admitting through a socket and
9
+ // spawning a process are things only a daemon has, so it is handed to her
10
+ // at birth in the class body the terrain brings; this body runs nobody and
11
+ // stands nowhere. Every row runs once the box is up.
24
12
  import { OWNER } from 'nervur';
25
- import type { Asker, JsonObject, OccupantRecord } from 'nervur';
13
+ import { contract } from './contract.ts';
14
+ import type { Asker, JsonObject, OccupantRecord, Stance } from 'nervur';
15
+ import type { Harborage } from './box.ts';
26
16
  import { Faculty } from './faculty.ts';
27
17
 
28
- // The name the dock ward holds her under.
29
- export const AGENTS = 'agents';
30
-
31
- // One agent. `id` is the client identity the avatar is known by in her ward.
32
- // A process row and a model row, told apart by what they name.
33
18
  export type Process = { command: string; args: string[]; dir: string };
34
19
  export type Model = { url: string; model: string; key?: string; turns?: number };
35
20
  export type Row = { id: string; ward: string } & (Process | Model);
36
21
  export const isProcess = (r: Row): r is { id: string; ward: string } & Process => 'command' in r;
37
22
 
38
- // What a daemon stands for a row: a side over her avatar, closed when the
23
+ // What a terrain stands for a row: a side over her avatar, closed when the
39
24
  // row is dropped or the box goes down.
40
25
  export type Side = { close(): Promise<void> };
41
26
  export type Runner = (row: Row) => Promise<Side>;
42
27
 
43
- // The root and nobody else, as the clock gates `keep`: an ask gated this way
44
- // is the device's own and cannot be reached through a door.
45
28
  const root = (_occ: OccupantRecord | undefined, asker: Asker) => asker.id === OWNER;
46
29
 
47
30
  const ROW = {
@@ -50,26 +33,36 @@ const ROW = {
50
33
  required: ['id', 'ward'],
51
34
  };
52
35
 
36
+ const ASKS = {
37
+ keep: { description: 'add an agent, or replace the one of that id: an identity, her ward, and what runs for her, a command with args in a folder, or a model at a url', input: ROW, for: root },
38
+ drop: { description: 'stop an agent and forget her row; the avatar in her ward stays until the root removes her', input: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'] }, for: root },
39
+ list: { description: 'every agent she holds', input: { type: 'object' }, for: root },
40
+ };
41
+
42
+ // What the box's own code needs from the faculty that runs its agents.
43
+ export const AGENTS = contract('agents', ASKS);
44
+
53
45
  export class Agents extends Faculty {
54
46
  static override cells: JsonObject = { watched: {}, minted: 0, agents: {} };
55
- static override asks = {
56
- keep: { description: 'add an agent, or replace the one of that id: an identity, her ward, and what runs for her, a command with args in a folder, or a model at a url', input: ROW, for: root },
57
- drop: { description: 'stop an agent and forget her row; the avatar in her ward stays until the root removes her', input: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'] }, for: root },
58
- list: { description: 'every agent she holds', input: { type: 'object' }, for: root },
59
- };
47
+ static override asks = ASKS;
48
+ static override fulfils = [AGENTS];
49
+ static override stands(): boolean {
50
+ return false;
51
+ }
60
52
 
61
- runner: Runner | undefined;
53
+ readonly runner: Runner | undefined;
62
54
  readonly running = new Map<string, Side>();
63
55
 
64
- private get rows(): Record<string, Row> {
65
- return this.cells.agents as unknown as Record<string, Row>;
56
+ constructor(stance: Stance, box?: Harborage, runner?: Runner) {
57
+ super(stance, box);
58
+ this.runner = runner;
59
+ this.box.up(async () => {
60
+ for (const row of Object.values(this.rows)) await this.#run(row);
61
+ });
66
62
  }
67
63
 
68
- // Every row up, once the daemon has handed the runner. Called after the
69
- // wards are hosted, since an avatar is admitted into one of them.
70
- async start(runner: Runner): Promise<void> {
71
- this.runner = runner;
72
- for (const row of Object.values(this.rows)) await this.#run(row);
64
+ private get rows(): Record<string, Row> {
65
+ return this.cells.agents as unknown as Record<string, Row>;
73
66
  }
74
67
 
75
68
  async #run(row: Row): Promise<void> {
@@ -110,7 +103,7 @@ export class Agents extends Faculty {
110
103
  }
111
104
 
112
105
  list(): JsonObject {
113
- return { agents: Object.values(this.rows) as unknown as JsonObject[] };
106
+ return { agents: Object.values(this.rows) };
114
107
  }
115
108
 
116
109
  // The device puts the box down: every side closes, the rows stay.
@@ -27,7 +27,7 @@
27
27
  // `quo-harbor.md`'s to say and not this file's. A stand-in in a test is an
28
28
  // ordinary HTTP/1.1 server, so what a test proves is the shape and the
29
29
  // signing rather than the transport, as the tab's push service did.
30
- import type { Ring, Token } from './push.ts';
30
+ import type { Ring, Token } from './ring.ts';
31
31
 
32
32
  const subtle = () => globalThis.crypto.subtle;
33
33
  const ES256 = { name: 'ECDSA', namedCurve: 'P-256' } as const;
@@ -83,9 +83,9 @@ export async function provider(a: Apns, made: { at: number; jwt: string } | null
83
83
  // The device a token names, or nothing a ring may send. It is checked
84
84
  // where a human can read why rather than by Apple two hops later.
85
85
  export function device(token: Token): string {
86
- const device = token.device;
87
- if (typeof device !== 'string' || !/^[0-9a-fA-F]{8,200}$/.test(device)) throw new Error('an apns token is a device token');
88
- return device;
86
+ const named = token.device;
87
+ if (typeof named !== 'string' || !/^[0-9a-fA-F]{8,200}$/.test(named)) throw new Error('an apns token is a device token');
88
+ return named;
89
89
  }
90
90
 
91
91
  // One ring, as Apple reads it: the path, the headers and the body. A ring
package/box/box.ts ADDED
@@ -0,0 +1,31 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // What the harbor is to a faculty: the three things only a box has, handed
3
+ // to every faculty at birth, so nobody writes into a faculty afterwards.
4
+ //
5
+ // `save` keeps her ward, for a faculty whose cells change outside any ask, a
6
+ // token issued by a route. `now` is the box's clock. `up` is work to do once
7
+ // the box is up, every ward hosted and every being settled, for a faculty
8
+ // whose work reaches the rest of the box, a line dialed or an agent run; the
9
+ // terrain that says the box is up waits for that work, so a box is up when
10
+ // it has landed, and work handed after that runs at once.
11
+ export type Harborage = { save(): Promise<void>; now(): number; up(work: () => Promise<void>): void };
12
+
13
+ // The harborage a faculty holds when none was handed: a suite's ward, which
14
+ // keeps nothing, reads the runtime's clock, and does its work at once.
15
+ export const ALONE: Harborage = { save: () => Promise.resolve(), now: () => Date.now(), up: (work) => void work().catch(() => {}) };
16
+
17
+ // A harborage over a ward's save, raised by the terrain once the box is up.
18
+ export function harborageOver(save: () => Promise<void>, now: () => number = () => Date.now()): Harborage & { raise(): Promise<void> } {
19
+ let waiting: (() => Promise<void>)[] | null = [];
20
+ const run = (work: () => Promise<void>) => work().catch(() => {});
21
+ return {
22
+ save,
23
+ now,
24
+ up: (work) => (waiting ? void waiting.push(work) : void run(work)),
25
+ raise: async () => {
26
+ const held = waiting ?? [];
27
+ waiting = null;
28
+ await Promise.all(held.map(run));
29
+ },
30
+ };
31
+ }
@@ -0,0 +1,41 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // A contract: what a caller needs from a faculty, said as a blueprint and
3
+ // nothing else, so it is written, read and hashed exactly as a describe is.
4
+ // It is known by its digest, and that digest is the name a box lends under.
5
+ // Nothing enforces it, as Quo enforces no blueprint: the caller reads the
6
+ // describe she was handed and decides with `covers`.
7
+ import { canonical, digest } from 'nervur';
8
+ import type { Ask, AskSpec, Blueprint } from 'nervur';
9
+
10
+ export type Contract = Blueprint;
11
+
12
+ // A contract out of the asks a class declares. Descriptions stay behind,
13
+ // since they are words for a reader and a reworded sentence must not move a
14
+ // name. The title is the contract's own word for itself.
15
+ export function contract(title: string, asks: Record<string, AskSpec>): Contract {
16
+ return {
17
+ asks: Object.entries(asks).map(([name, spec]) => ({ name, input: spec.input ?? { type: 'object' }, ...(spec.output !== undefined ? { output: spec.output } : {}) })),
18
+ notes: { title },
19
+ };
20
+ }
21
+
22
+ // The name a contract is lent under.
23
+ export const nameOf = (c: Contract): Promise<string> => digest(c);
24
+
25
+ // The word a contract calls itself.
26
+ export const titleOf = (c: Contract): string => {
27
+ const t = (c.notes as { title?: unknown } | null)?.title;
28
+ return typeof t === 'string' ? t : '';
29
+ };
30
+
31
+ // Whether a blueprint covers a contract, ask by ask: every ask the contract
32
+ // names stands in it with the same input, and the same output where the
33
+ // contract says one, in the canonical form. It may carry asks of its own.
34
+ export function covers(held: Blueprint, c: Contract): boolean {
35
+ const byName = new Map<string, Ask>(held.asks.map((a) => [a.name, a]));
36
+ return c.asks.every((want) => {
37
+ const got = byName.get(want.name);
38
+ if (!got || canonical(got.input) !== canonical(want.input)) return false;
39
+ return want.output === undefined || (got.output !== undefined && canonical(got.output) === canonical(want.output));
40
+ });
41
+ }