@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
@@ -1,5 +1,10 @@
1
1
  import { Avatar, PUSHER, USER } from './avatar.js';
2
- import { FACULTIES, join } from '../dock/index.js';
2
+ import { LOCATION } from '../box/location.js';
3
+ import { NOTIFICATIONS } from '../box/notifications.js';
4
+ import { PUSH } from '../box/push.js';
5
+ import { TIMER } from '../box/timer.js';
6
+ import { join } from '../box/join.js';
7
+ import { titleOf } from '../box/contract.js';
3
8
  export class Presence extends Avatar {
4
9
  // Her world's user being sees the box, and nobody else: the way back she
5
10
  // handed over at the join is the one occupant who may ask through her.
@@ -12,10 +17,14 @@ export class Presence extends Avatar {
12
17
  static skips(id) {
13
18
  return id === USER;
14
19
  }
15
- // Which names the box lent her, by name, settled after birth.
20
+ // What a device can do that a world may want: the contracts she asks her
21
+ // box for, each held under the word the contract calls itself.
22
+ static needs = [LOCATION, NOTIFICATIONS, PUSH, TIMER];
23
+ // Which of them the box lent her, by that word, settled after birth.
16
24
  lent;
17
25
  constructor(stance) {
18
26
  super(stance);
19
- this.lent = Promise.all(Object.keys(FACULTIES).map(async (name) => [name, await join(this, name)])).then((all) => Object.fromEntries(all));
27
+ const C = this.constructor;
28
+ this.lent = Promise.all(C.needs.map(async (c) => [titleOf(c), await join(this, c, titleOf(c))])).then((all) => Object.fromEntries(all));
20
29
  }
21
30
  }
@@ -1,3 +1,6 @@
1
1
  import type { Hosted } from 'nervur/harbor';
2
- export declare function setupHome(hosted: Hosted, user: string): Promise<void>;
3
- export declare function setup(hosted: Hosted, user: string, cls?: string): Promise<void>;
2
+ export type Setup = {
3
+ front: string;
4
+ home: string;
5
+ };
6
+ export declare function setup(hosted: Hosted, user: string, classes?: Partial<Setup>): Promise<void>;
@@ -1,11 +1,12 @@
1
1
  import { DESK, HOME } from './user.js';
2
+ const SETUP = { front: 'beings/Desk', home: 'beings/User' };
2
3
  // The root boots and steps back: it invites an owner on the ward's own pk
3
- // and the user being knocks for it, so from then on placing a being,
4
+ // and the home being knocks for it, so from then on placing a being,
4
5
  // minting a way in or removing one is an ask on her, shown by her gate to a
5
6
  // device that may mint, and the box is not logged into for it. The same
6
7
  // standing a user being of another ward takes to own this one; here it is
7
8
  // her own.
8
- export async function setupHome(hosted, user) {
9
+ async function setupHome(hosted, user) {
9
10
  const inv = (await hosted.ask('invite', { being: hosted.pk, id: HOME }));
10
11
  if ('error' in inv)
11
12
  throw new Error(`home: ${inv.error}`);
@@ -13,27 +14,27 @@ export async function setupHome(hosted, user) {
13
14
  if (placed.taken !== HOME)
14
15
  throw new Error(`home: ${placed.error ?? 'she could not take her own door'}`);
15
16
  }
16
- // The ward's home being is a `User` unless the device says which class: an
17
- // organisation's ward has an org's home being, of the estate's own class,
18
- // and everything else of setup, the desk and the door, is placed on her the
19
- // same way.
20
- export async function setup(hosted, user, cls = 'User') {
21
- if (!/^[\w.-]+$/.test(user) || user === 'desk')
22
- throw new Error(`a user is a word, and not desk: ${user}`);
17
+ // Everything setup places is placed the same way whatever the classes are:
18
+ // a home key the front already holds is refused by the ward, as any key
19
+ // taken is.
20
+ export async function setup(hosted, user, classes = {}) {
21
+ const { front, home } = { ...SETUP, ...classes };
22
+ if (!/^[\w.-]+$/.test(user))
23
+ throw new Error(`a user is a word: ${user}`);
23
24
  const boot = async (args) => {
24
25
  const out = (await hosted.ask('boot', args));
25
26
  if (out.error)
26
27
  throw new Error(`init: ${out.error}`);
27
28
  };
28
- await boot({ key: 'desk', class: 'Desk' });
29
- const pub = (await hosted.ask('public', { key: 'desk' }));
29
+ await boot({ key: DESK, class: front });
30
+ const pub = (await hosted.ask('public', { key: DESK }));
30
31
  if (pub.error)
31
32
  throw new Error(`init: ${pub.error}`);
32
- await boot({ key: user, class: cls });
33
+ await boot({ key: user, class: home });
33
34
  const inv = (await hosted.ask('invite', { being: user, id: DESK }));
34
- // the desk's first hello names her, since a being does not know her own key
35
- const placed = (await hosted.ask('knock', { being: 'desk', id: `user:${user}`, invitation: inv, method: 'hello', args: { name: user } }));
35
+ // the front's first hello names her, since a being does not know her own key
36
+ const placed = (await hosted.ask('knock', { being: DESK, id: `user:${user}`, invitation: inv, method: 'hello', args: { name: user } }));
36
37
  if (placed.taken !== `user:${user}`)
37
- throw new Error('init: the desk could not reach the user being');
38
+ throw new Error('init: the front could not reach the home being');
38
39
  await setupHome(hosted, user);
39
40
  }
@@ -1,5 +1,5 @@
1
- import type { Asker, Blueprint, Invitation, JsonObject, OccupantRecord, Stance } from 'nervur';
2
- import { Carrier } from './carry.ts';
1
+ import { Being } from 'nervur';
2
+ import type { Asker, Blueprint, Invitation, JsonObject, OccupantRecord, Reply, Stance } from 'nervur';
3
3
  export declare const DESK = "desk";
4
4
  export declare const BELL = "bell";
5
5
  export declare const HOME = "home";
@@ -8,11 +8,12 @@ declare const rootMinted: (occ: OccupantRecord | undefined) => boolean;
8
8
  declare const mayWake: (occ: OccupantRecord | undefined) => boolean;
9
9
  declare const mayMint: (occ: OccupantRecord | undefined) => boolean;
10
10
  declare const mayRevoke: (occ: OccupantRecord | undefined) => boolean;
11
- export declare class User extends Carrier {
11
+ export declare class User extends Being {
12
12
  #private;
13
13
  static carries(occ: OccupantRecord | undefined): boolean;
14
14
  static hidden(id: string): boolean;
15
15
  static shows(id: string, occ: OccupantRecord | undefined): boolean;
16
+ answer(asker: Asker, method?: string, args?: JsonObject): Promise<Reply>;
16
17
  static cells: {
17
18
  name: string;
18
19
  reports: JsonObject[];
@@ -7,9 +7,13 @@
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 { Carrier } from './carry.js';
12
- import { PUSH, banner, isToken, join, sealBanner } from '../dock/index.js';
10
+ import { Being, isInvitation, isSilence, isWord, wordOf } from 'nervur';
11
+ import { CARRIES, carry } from './carry.js';
12
+ import { banner, isToken, sealBanner } from '../box/index.js';
13
+ import { PUSH as PUSHING } from '../box/push.js';
14
+ import { join } from '../box/join.js';
15
+ // Her standing at the box's push faculty, under an id of her own.
16
+ const PUSH = 'push';
13
17
  export const DESK = 'desk';
14
18
  // The prefix her doorbell standings are filed under, one per device: the
15
19
  // way she rings that device, and never a way anything else may ask on.
@@ -61,12 +65,12 @@ const mayMint = (occ) => isDesk(occ) || (isDevice(occ) && held(occ).mint);
61
65
  // it has the act out of reach and the root's own removals instead, and that
62
66
  // is a choice an org makes rather than one the dock makes for it.
63
67
  const mayRevoke = (occ) => isDesk(occ) || (isDevice(occ) && held(occ).revoke);
64
- // She carries her standings, acme, the calendar, the house, for a device the
65
- // human allowed to reach them at the exchange: the note says so. A device
66
- // without the note sees her own asks alone, and a model sees acme only
68
+ // She carries her standings, her org, the calendar, the house, for a device
69
+ // the human allowed to reach them at the exchange: the note says so. A device
70
+ // without the note sees her own asks alone, and a model sees the org only
67
71
  // because the human said it may. A device the root minted reaches: the root
68
72
  // is the owner of everything she holds and chose to mint it.
69
- export class User extends Carrier {
73
+ export class User extends Being {
70
74
  static carries(occ) {
71
75
  return isDevice(occ) && held(occ).reach;
72
76
  }
@@ -75,20 +79,23 @@ export class User extends Carrier {
75
79
  // not bound yet, a phone in a tunnel. Nothing delivered spends nothing,
76
80
  // so the invitation is tried again at the next push.
77
81
  // The bell standing is plumbing and carries nothing: a device that may
78
- // reach what she holds sees acme and the house, and never the envoy that
82
+ // reach what she holds sees the org and the house, and never the envoy that
79
83
  // wakes it. Her ways back are plumbing too, and a way back to a presence
80
84
  // being carries that device's faculties, `to:phone-location-where`, which
81
85
  // is how a model at home learns where the human is.
82
86
  static hidden(id) {
83
- return super.hidden(id) || id === PUSH || id.startsWith(`${BELL}:`);
87
+ return CARRIES.hidden(id) || id === PUSH || id.startsWith(`${BELL}:`);
84
88
  }
85
89
  // Her own door is carried to a device that may mint, and to nobody else:
86
90
  // `mint` is the word for a device trusted to change who is in her world,
87
91
  // and owning the world from that device is that trust said in full. A
88
- // device that reaches sees acme and the house, and no `home-boot`.
92
+ // device that reaches sees the org and the house, and no `home-boot`.
89
93
  static shows(id, occ) {
90
94
  return id !== HOME || held(occ).mint;
91
95
  }
96
+ answer(asker, method, args = {}) {
97
+ return carry(this, asker, method, args, (a, m, g) => super.answer(a, m, g));
98
+ }
92
99
  static cells = { name: '', reports: [], ways: {}, outbox: {} };
93
100
  // Her standing at the box's push faculty, taken again at every birth: a
94
101
  // standing points at a being and not at a box, and a ward that moved is
@@ -103,7 +110,7 @@ export class User extends Carrier {
103
110
  #push;
104
111
  constructor(stance) {
105
112
  super(stance);
106
- this.#push = join(this, PUSH);
113
+ this.#push = join(this, PUSHING, PUSH);
107
114
  }
108
115
  // A world whose own being she is, is a home: a device reaching her holds
109
116
  // a standing at home and not a membership, and she says so in her notes,
@@ -1,6 +1,6 @@
1
- import type { Asker, JsonObject, OccupantRecord } from 'nervur';
1
+ import type { Asker, JsonObject, OccupantRecord, Stance } from 'nervur';
2
+ import type { Harborage } from './box.ts';
2
3
  import { Faculty } from './faculty.ts';
3
- export declare const AGENTS = "agents";
4
4
  export type Process = {
5
5
  command: string;
6
6
  args: string[];
@@ -25,6 +25,7 @@ export type Side = {
25
25
  };
26
26
  export type Runner = (row: Row) => Promise<Side>;
27
27
  declare const root: (_occ: OccupantRecord | undefined, asker: Asker) => boolean;
28
+ export declare const AGENTS: import("nervur").Blueprint;
28
29
  export declare class Agents extends Faculty {
29
30
  #private;
30
31
  static cells: JsonObject;
@@ -90,10 +91,12 @@ export declare class Agents extends Faculty {
90
91
  for: typeof root;
91
92
  };
92
93
  };
93
- runner: Runner | undefined;
94
+ static fulfils: import("nervur").Blueprint[];
95
+ static stands(): boolean;
96
+ readonly runner: Runner | undefined;
94
97
  readonly running: Map<string, Side>;
98
+ constructor(stance: Stance, box?: Harborage, runner?: Runner);
95
99
  private get rows();
96
- start(runner: Runner): Promise<void>;
97
100
  keep(args: JsonObject): Promise<JsonObject>;
98
101
  drop(args: JsonObject): Promise<JsonObject>;
99
102
  list(): JsonObject;
@@ -1,58 +1,51 @@
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';
13
+ import { contract } from './contract.js';
25
14
  import { Faculty } from './faculty.js';
26
- // The name the dock ward holds her under.
27
- export const AGENTS = 'agents';
28
15
  export const isProcess = (r) => 'command' in r;
29
- // The root and nobody else, as the clock gates `keep`: an ask gated this way
30
- // is the device's own and cannot be reached through a door.
31
16
  const root = (_occ, asker) => asker.id === OWNER;
32
17
  const ROW = {
33
18
  type: 'object',
34
19
  properties: { id: { type: 'string' }, ward: { type: 'string' }, command: { type: 'string' }, args: { type: 'array', items: { type: 'string' } }, dir: { type: 'string' }, url: { type: 'string' }, model: { type: 'string' }, key: { type: 'string' }, turns: { type: 'number' } },
35
20
  required: ['id', 'ward'],
36
21
  };
22
+ const ASKS = {
23
+ 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 },
24
+ 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 },
25
+ list: { description: 'every agent she holds', input: { type: 'object' }, for: root },
26
+ };
27
+ // What the box's own code needs from the faculty that runs its agents.
28
+ export const AGENTS = contract('agents', ASKS);
37
29
  export class Agents extends Faculty {
38
30
  static cells = { watched: {}, minted: 0, agents: {} };
39
- static asks = {
40
- 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 },
41
- 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 },
42
- list: { description: 'every agent she holds', input: { type: 'object' }, for: root },
43
- };
31
+ static asks = ASKS;
32
+ static fulfils = [AGENTS];
33
+ static stands() {
34
+ return false;
35
+ }
44
36
  runner;
45
37
  running = new Map();
38
+ constructor(stance, box, runner) {
39
+ super(stance, box);
40
+ this.runner = runner;
41
+ this.box.up(async () => {
42
+ for (const row of Object.values(this.rows))
43
+ await this.#run(row);
44
+ });
45
+ }
46
46
  get rows() {
47
47
  return this.cells.agents;
48
48
  }
49
- // Every row up, once the daemon has handed the runner. Called after the
50
- // wards are hosted, since an avatar is admitted into one of them.
51
- async start(runner) {
52
- this.runner = runner;
53
- for (const row of Object.values(this.rows))
54
- await this.#run(row);
55
- }
56
49
  async #run(row) {
57
50
  if (!this.runner)
58
51
  return;
@@ -1,4 +1,4 @@
1
- import type { Ring, Token } from './push.ts';
1
+ import type { Ring, Token } from './ring.ts';
2
2
  export declare function pkcs8(pem: string): Uint8Array;
3
3
  export type Apns = {
4
4
  key: string;
@@ -45,10 +45,10 @@ export async function provider(a, made) {
45
45
  // The device a token names, or nothing a ring may send. It is checked
46
46
  // where a human can read why rather than by Apple two hops later.
47
47
  export function device(token) {
48
- const device = token.device;
49
- if (typeof device !== 'string' || !/^[0-9a-fA-F]{8,200}$/.test(device))
48
+ const named = token.device;
49
+ if (typeof named !== 'string' || !/^[0-9a-fA-F]{8,200}$/.test(named))
50
50
  throw new Error('an apns token is a device token');
51
- return device;
51
+ return named;
52
52
  }
53
53
  // One ring, as Apple reads it: the path, the headers and the body. A ring
54
54
  // with a banner is an alert Apple must show, and `mutable-content` is what
@@ -0,0 +1,9 @@
1
+ export type Harborage = {
2
+ save(): Promise<void>;
3
+ now(): number;
4
+ up(work: () => Promise<void>): void;
5
+ };
6
+ export declare const ALONE: Harborage;
7
+ export declare function harborageOver(save: () => Promise<void>, now?: () => number): Harborage & {
8
+ raise(): Promise<void>;
9
+ };
@@ -0,0 +1,18 @@
1
+ // The harborage a faculty holds when none was handed: a suite's ward, which
2
+ // keeps nothing, reads the runtime's clock, and does its work at once.
3
+ export const ALONE = { save: () => Promise.resolve(), now: () => Date.now(), up: (work) => void work().catch(() => { }) };
4
+ // A harborage over a ward's save, raised by the terrain once the box is up.
5
+ export function harborageOver(save, now = () => Date.now()) {
6
+ let waiting = [];
7
+ const run = (work) => work().catch(() => { });
8
+ return {
9
+ save,
10
+ now,
11
+ up: (work) => (waiting ? void waiting.push(work) : void run(work)),
12
+ raise: async () => {
13
+ const held = waiting ?? [];
14
+ waiting = null;
15
+ await Promise.all(held.map(run));
16
+ },
17
+ };
18
+ }
@@ -0,0 +1,6 @@
1
+ import type { AskSpec, Blueprint } from 'nervur';
2
+ export type Contract = Blueprint;
3
+ export declare function contract(title: string, asks: Record<string, AskSpec>): Contract;
4
+ export declare const nameOf: (c: Contract) => Promise<string>;
5
+ export declare const titleOf: (c: Contract) => string;
6
+ export declare function covers(held: Blueprint, c: Contract): boolean;
@@ -0,0 +1,35 @@
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
+ // A contract out of the asks a class declares. Descriptions stay behind,
9
+ // since they are words for a reader and a reworded sentence must not move a
10
+ // name. The title is the contract's own word for itself.
11
+ export function contract(title, asks) {
12
+ return {
13
+ asks: Object.entries(asks).map(([name, spec]) => ({ name, input: spec.input ?? { type: 'object' }, ...(spec.output !== undefined ? { output: spec.output } : {}) })),
14
+ notes: { title },
15
+ };
16
+ }
17
+ // The name a contract is lent under.
18
+ export const nameOf = (c) => digest(c);
19
+ // The word a contract calls itself.
20
+ export const titleOf = (c) => {
21
+ const t = c.notes?.title;
22
+ return typeof t === 'string' ? t : '';
23
+ };
24
+ // Whether a blueprint covers a contract, ask by ask: every ask the contract
25
+ // names stands in it with the same input, and the same output where the
26
+ // contract says one, in the canonical form. It may carry asks of its own.
27
+ export function covers(held, c) {
28
+ const byName = new Map(held.asks.map((a) => [a.name, a]));
29
+ return c.asks.every((want) => {
30
+ const got = byName.get(want.name);
31
+ if (!got || canonical(got.input) !== canonical(want.input))
32
+ return false;
33
+ return want.output === undefined || (got.output !== undefined && canonical(got.output) === canonical(want.output));
34
+ });
35
+ }
@@ -1,11 +1,12 @@
1
- import type { Asker, JsonObject, OccupantRecord } from 'nervur';
1
+ import type { Asker, JsonObject, OccupantRecord, Stance } from 'nervur';
2
2
  import { type Dialer, type Harbor } from 'nervur/harbor';
3
+ import type { Harborage } from './box.ts';
3
4
  import { Faculty } from './faculty.ts';
4
- export declare const DIALERS = "dialers";
5
5
  export type Row = {
6
6
  url: string;
7
7
  };
8
8
  declare const root: (_occ: OccupantRecord | undefined, asker: Asker) => boolean;
9
+ export declare const DIALERS: import("nervur").Blueprint;
9
10
  export declare class Dialers extends Faculty {
10
11
  #private;
11
12
  static cells: JsonObject;
@@ -44,10 +45,12 @@ export declare class Dialers extends Faculty {
44
45
  for: typeof root;
45
46
  };
46
47
  };
47
- harbor: Harbor | undefined;
48
+ static fulfils: import("nervur").Blueprint[];
49
+ static stands(): boolean;
50
+ readonly harbor: Harbor | undefined;
48
51
  readonly open: Map<string, Dialer>;
52
+ constructor(stance: Stance, box?: Harborage, harbor?: Harbor);
49
53
  private get rows();
50
- start(harbor: Harbor): void;
51
54
  keep(args: JsonObject): JsonObject;
52
55
  drop(args: JsonObject): JsonObject;
53
56
  list(): JsonObject;
@@ -0,0 +1,76 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // The box's lines: a socket held to another box's quo. route, one per row,
3
+ // announcing this harbor's wards on open and reconnecting when a line drops.
4
+ // The rows are her cells, placed by the root with `keep`. The harbor she
5
+ // dials for is the terrain's, handed at birth in the class body it brings;
6
+ // this body dials nothing and stands nowhere. Every row is dialed once the
7
+ // box is up, and a row kept after that is dialed at once.
8
+ import { OWNER } from 'nervur';
9
+ import { dial } from 'nervur/harbor';
10
+ import { contract } from './contract.js';
11
+ import { Faculty } from './faculty.js';
12
+ const root = (_occ, asker) => asker.id === OWNER;
13
+ const URL_ = { type: 'object', properties: { url: { type: 'string' } }, required: ['url'] };
14
+ const ASKS = {
15
+ keep: { description: 'hold a socket to that quo. route, from now and from every boot, until it is dropped', input: URL_, for: root },
16
+ drop: { description: 'close the socket to that route and forget it', input: URL_, for: root },
17
+ list: { description: 'every route she dials, and whether the line is open now', input: { type: 'object' }, for: root },
18
+ };
19
+ // What the box's own code needs from the faculty that holds its lines.
20
+ export const DIALERS = contract('dialers', ASKS);
21
+ export class Dialers extends Faculty {
22
+ static cells = { watched: {}, minted: 0, dialers: {} };
23
+ static asks = ASKS;
24
+ static fulfils = [DIALERS];
25
+ static stands() {
26
+ return false;
27
+ }
28
+ harbor;
29
+ open = new Map();
30
+ #up = false;
31
+ constructor(stance, box, harbor) {
32
+ super(stance, box);
33
+ this.harbor = harbor;
34
+ this.box.up(() => {
35
+ this.#up = true;
36
+ for (const row of Object.values(this.rows))
37
+ this.#dial(row.url);
38
+ return Promise.resolve();
39
+ });
40
+ }
41
+ get rows() {
42
+ return this.cells.dialers;
43
+ }
44
+ #dial(url) {
45
+ if (!this.harbor || !this.#up || this.open.has(url))
46
+ return;
47
+ this.open.set(url, dial(this.harbor, url));
48
+ }
49
+ keep(args) {
50
+ const url = args.url;
51
+ if (typeof url !== 'string' || !/^https?:\/\/\S+$/.test(url))
52
+ return { error: 'url is the quo. route of the harbor dialed, over http or https' };
53
+ const clean = url.replace(/\/$/, '');
54
+ this.rows[clean] = { url: clean };
55
+ this.#dial(clean);
56
+ return { kept: clean };
57
+ }
58
+ drop(args) {
59
+ const url = typeof args.url === 'string' ? args.url.replace(/\/$/, '') : '';
60
+ if (!this.rows[url])
61
+ return { error: 'no such route' };
62
+ this.open.get(url)?.close();
63
+ this.open.delete(url);
64
+ delete this.rows[url];
65
+ return { dropped: url };
66
+ }
67
+ list() {
68
+ return { dialers: Object.keys(this.rows).map((url) => ({ url, open: this.open.get(url)?.socket !== null && this.open.has(url) })) };
69
+ }
70
+ // The device puts the box down: every line closes, the rows stay.
71
+ close() {
72
+ for (const d of this.open.values())
73
+ d.close();
74
+ this.open.clear();
75
+ }
76
+ }