@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
package/box/timer.ts ADDED
@@ -0,0 +1,150 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // The box's clock, and the one being on a device that reads one. A being who
3
+ // could read a clock could not be moved, since the moment she read would be
4
+ // her box's and no other, so the moment reaches every other being the one way
5
+ // anything reaches a being: as an ask at her door, from this faculty, as the
6
+ // occupant she named when she asked to be woken.
7
+ //
8
+ // She arms for the next due moment and for nothing else. Every watcher
9
+ // answers a round with when she is next due, this arms for the earliest of
10
+ // them, and a box whose watchers all say null arms nothing. A round is armed
11
+ // for one moment at a time and never overlaps the one before, and the moment
12
+ // is floored to the minute with half a second after it, since a watcher
13
+ // floors the moment she is handed.
14
+ //
15
+ // One method knows a terrain, `arm`, and a subclass replaces it: a
16
+ // `setTimeout` here, the object's storage alarm on a worker. Her moment is
17
+ // the box's clock.
18
+ import type { JsonObject } from 'nervur';
19
+ import { contract } from './contract.ts';
20
+ import { Faculty, WATCH, type Woke } from './faculty.ts';
21
+
22
+ const ASKS = {
23
+ watch: WATCH,
24
+ due: { description: 'bring my next round forward to this moment', input: { type: 'object', properties: { at: { type: 'number' } }, required: ['at'] } },
25
+ now: { description: 'this box’s moment', input: { type: 'object' } },
26
+ };
27
+
28
+ // What a being needs from a box that can wake her at a moment.
29
+ export const TIMER = contract('timer', ASKS);
30
+
31
+ const MINUTE = 60_000;
32
+ // A timeout further out than this is not trusted, so the arming is capped and
33
+ // made again when it goes off.
34
+ const DAY = 86_400_000;
35
+
36
+ export class Timer extends Faculty {
37
+ static override asks = ASKS;
38
+ static override fulfils = [TIMER];
39
+
40
+ #timeout: ReturnType<typeof setTimeout> | undefined;
41
+ #stopped = false;
42
+ #armed: number | null = null;
43
+ #round: Promise<Woke[]> | null = null;
44
+
45
+ // The moment she is armed for, or null while she is asleep.
46
+ get armed(): number | null {
47
+ return this.#armed;
48
+ }
49
+
50
+ // A watcher says she is due sooner than the box knew. It only ever brings
51
+ // the round forward.
52
+ due(args: JsonObject): JsonObject {
53
+ const at = args.at;
54
+ if (typeof at !== 'number' || !Number.isFinite(at)) return { error: 'a due carries the moment: { at }' };
55
+ const when = moment(at);
56
+ if (this.#armed === null || when < this.#armed) this.#arm(when);
57
+ return { armed: this.#armed };
58
+ }
59
+
60
+ // Whoever was just filed is ticked at once, and what she answers is what
61
+ // the box arms for, so she is never waiting for a round nobody scheduled.
62
+ protected override async welcomed(id: string): Promise<void> {
63
+ await this.round(this.box.now(), id);
64
+ }
65
+
66
+ // The arming went off. A moment still well ahead is the day's cap, so it is
67
+ // armed again rather than fired.
68
+ async rang(): Promise<Woke[]> {
69
+ if (this.#stopped) return [];
70
+ if (this.#armed !== null && this.#armed - this.box.now() > 1_000) {
71
+ this.arm(this.#armed);
72
+ return [];
73
+ }
74
+ return this.fire(this.box.now());
75
+ }
76
+
77
+ // The device's own call at a moment it names: how a suite proves a wake
78
+ // without waiting for one and how an operator makes a round happen now.
79
+ fire(now: number): Promise<Woke[]> {
80
+ return this.round(now);
81
+ }
82
+
83
+ // One round, and never two at once.
84
+ protected round(now: number, only?: string): Promise<Woke[]> {
85
+ if (this.#round) return this.#round;
86
+ const mine = this.#run(now, only);
87
+ this.#round = mine;
88
+ return mine.finally(() => {
89
+ if (this.#round === mine) this.#round = null;
90
+ });
91
+ }
92
+
93
+ async #run(now: number, only?: string): Promise<Woke[]> {
94
+ if (only === undefined) this.#arm(null);
95
+ const woke = await this.woke((w) => ({ ...w.args, now }), only);
96
+ if (woke === null) {
97
+ this.stop();
98
+ return [];
99
+ }
100
+ // A moment already gone is no arming.
101
+ const named = soonest(woke);
102
+ const said = named !== null && named > this.box.now() ? named : null;
103
+ const held = this.#armed;
104
+ const at = said === null ? held : held === null ? moment(said) : Math.min(held, moment(said));
105
+ if (at !== null && at !== this.#armed) this.#arm(at);
106
+ else if (at === null && this.#armed !== null) this.#arm(null);
107
+ return woke;
108
+ }
109
+
110
+ #arm(when: number | null): void {
111
+ if (this.#stopped) return;
112
+ this.#armed = when;
113
+ this.arm(when);
114
+ }
115
+
116
+ // The one thing a terrain says about itself: be woken at this moment, or at
117
+ // no moment at all. Here a timeout, capped at a day, unreferenced so the box
118
+ // stays up for its work and never for this.
119
+ protected arm(when: number | null): void {
120
+ clearTimeout(this.#timeout);
121
+ this.#timeout = undefined;
122
+ if (when === null) return;
123
+ this.#timeout = setTimeout(() => void this.rang(), Math.min(Math.max(when - this.box.now(), 0), DAY));
124
+ this.#timeout.unref?.();
125
+ }
126
+
127
+ // The device puts the box down.
128
+ stop(): void {
129
+ this.#stopped = true;
130
+ this.#armed = null;
131
+ this.arm(null);
132
+ }
133
+
134
+ now(): JsonObject {
135
+ return { now: this.box.now() };
136
+ }
137
+ }
138
+
139
+ const moment = (at: number): number => Math.max(at, Math.floor(at / MINUTE) * MINUTE + 500);
140
+
141
+ function soonest(woke: Woke[]): number | null {
142
+ let at: number | null = null;
143
+ for (const w of woke) {
144
+ if (w.out === null || typeof w.out !== 'object') continue;
145
+ const next = (w.out as { next?: unknown }).next;
146
+ if (typeof next !== 'number' || !Number.isFinite(next)) continue;
147
+ if (at === null || next < at) at = next;
148
+ }
149
+ return at;
150
+ }
package/box/tree.ts ADDED
@@ -0,0 +1,22 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Classes as a harbor is handed them: a tree whose branches are folders and
3
+ // whose leaves are exports, the way a repository holds them. A module
4
+ // namespace is a branch, so `{ box: await import('./box/index.ts') }`
5
+ // is a tree, and so is a bundle's nested exports. The repository is a
6
+ // harbor's DNA; the cells are its memory.
7
+ import type { BeingClass } from 'nervur';
8
+
9
+ export type Tree = { readonly [name: string]: unknown };
10
+
11
+ // Every class in a tree, keyed by where it lives: the folders down to it and
12
+ // its export name, `box/Dock`. The key is the class's identity in a ward,
13
+ // as a being's key is hers, and it moves only when the class moves.
14
+ export function keyed(tree: Tree, at = ''): Record<string, BeingClass> {
15
+ const out: Record<string, BeingClass> = {};
16
+ for (const [name, v] of Object.entries(tree)) {
17
+ const key = at ? `${at}/${name}` : name;
18
+ if (typeof v === 'function' && 'prototype' in v) out[key] = v as BeingClass;
19
+ else if (v !== null && typeof v === 'object' && !Array.isArray(v)) Object.assign(out, keyed(v as Tree, key));
20
+ }
21
+ return out;
22
+ }
@@ -12,7 +12,7 @@
12
12
  // `auth` of the subscription it made, so the one party a banner key would
13
13
  // hide the line from on this path is the push service, which that
14
14
  // encryption already excludes. A second seal would buy nothing and would
15
- // make every tab stand a dock ward to keep a banner key in. So the world
15
+ // make every tab stand a box ward to keep a banner key in. So the world
16
16
  // encrypts here, the browser decrypts, and the service worker shows what it
17
17
  // was handed rather than a line it guessed at.
18
18
  //
@@ -23,7 +23,7 @@
23
23
  // This is one ground of the three the bell has. APNs and FCM are the
24
24
  // stores' own and want a platform key; this one stands where nothing is in
25
25
  // the way, a keypair the world mints itself.
26
- import type { Line, Ring, Token } from './push.ts';
26
+ import type { Line, Ring, Token } from './ring.ts';
27
27
 
28
28
  const subtle = () => globalThis.crypto.subtle;
29
29
  const ES256 = { name: 'ECDSA', namedCurve: 'P-256' } as const;
package/cli/client.ts CHANGED
@@ -5,7 +5,7 @@ import { connect } from 'node:net';
5
5
  import { createInterface } from 'node:readline';
6
6
  import { sockPath, sidePath } from './daemon.ts';
7
7
 
8
- export type Answer = { result: unknown } | { error: string };
8
+ export type Answer = { result: unknown; ward?: string } | { error: string };
9
9
 
10
10
  // `via` names a standing the ward's user being holds at another ward: the
11
11
  // ask is then an owner's ask at that ward's door, sealed, and the answer is
package/cli/daemon.ts CHANGED
@@ -32,11 +32,11 @@
32
32
  // <- { ok: as } | { error }, then one line per final text
33
33
  //
34
34
  // The proof for a local side is that it reached side.sock: the daemon mints
35
- // a nonce for the connection on the `local` faculty of its dock ward, the
35
+ // a nonce for the connection on the `local` faculty of its box ward, the
36
36
  // box's own way in, and the desk trades it at a standing there, once. A
37
37
  // `local` proof arriving any other way names no nonce and is refused.
38
38
  //
39
- // Agents have no door: the `agents` faculty of the dock ward holds each
39
+ // Agents have no door: the `agents` faculty of the box ward holds each
40
40
  // agent's identity, her ward, her command and her folder, placed by the
41
41
  // root with `keep` as a schedule is placed on the clock, and the daemon
42
42
  // admits her on the local proof and runs the agent side beside her avatar
@@ -80,13 +80,12 @@
80
80
  // through the droplet it dialed.
81
81
  import { createServer, type Server, type Socket } from 'node:net';
82
82
  import type { Server as HttpServer } from 'node:http';
83
- import { unlink, chmod, mkdir, readFile, writeFile } from 'node:fs/promises';
84
- import { existsSync } from 'node:fs';
83
+ import { unlink, chmod } from 'node:fs/promises';
85
84
  import { createHash, randomBytes } from 'node:crypto';
86
85
  import { tmpdir } from 'node:os';
87
86
  import { join, resolve } from 'node:path';
88
87
  import { createInterface } from 'node:readline';
89
- import { isSilence, isWord, wordOf, type Invitation, type JsonObject } from 'nervur';
88
+ import { isSilence, isWord, wordOf, type Invitation, type JsonObject, type Stance } from 'nervur';
90
89
  import type { JSONRPCMessage } from '@modelcontextprotocol/sdk/types.js';
91
90
  import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
92
91
  import { DiskHarbor, defaultWard, type Hosted } from '../harbor/disk.ts';
@@ -94,12 +93,10 @@ import type { Harbor } from 'nervur/harbor';
94
93
  import type { Avatar, User } from '../beings/index.ts';
95
94
  // an identity in a world: the three asks any terrain's routes make at a ward
96
95
  export { avatarOf, letIn, find } from '../beings/join.ts';
97
- import { avatarOf, letIn, find } from '../beings/join.ts';
98
- import { DOCK, FACULTIES, LOCAL, PUSH, TIMER, Local, Push, Timer, facultyOf, publicOf, routed, settled, webPush, type Ring } from '../dock/index.ts';
99
- import { apnsHttp2, apnsOf } from '../dock/http2.ts';
100
- import { GRANTS, Grants } from '../dock/grants.ts';
101
- import { AGENTS, Agents, isProcess } from '../dock/agents.ts';
102
- import { DIALERS, Dialers } from '../dock/dialers.ts';
96
+ import { avatarOf, isIdentity, letIn, find } from '../beings/join.ts';
97
+ import { Http as Wire, routed, webPush, type Ring } from '../box/index.ts';
98
+ import { apnsHttp2, apnsOf } from '../box/http2.ts';
99
+ import { AGENTS, Agents, BOX, BOX_SEED, DIALERS, Dialers, GRANTS, Grants, LOCAL, Local, PUSH, Push, TIMER, Timer, WIRE, boxOf, isProcess, keyLent, lent, settled, type Harborage } from '../box/index.ts';
103
100
  import { mcpSide } from '../mcp/server.ts';
104
101
  import { runnerSide, type Model } from '../mcp/runner.ts';
105
102
  import { agentSide, processRun } from '../mcp/agent.ts';
@@ -118,7 +115,10 @@ import { listenHttp, type Http, type Quo } from '../harbor/http.ts';
118
115
  export type { Routes } from '../mcp/route.ts';
119
116
  export type Serving = { harbor: DiskHarbor; sock: string; side: string; http: Http | null; grants: Grants | null; oauth: OAuth | null; exchange: Exchange | null; mcp: McpHttp | null; agents: Agents | null; quo: Quo; tick(now: number): Promise<void>; close(): Promise<void> };
120
117
  // `rings` is what a test puts in the push faculty beside what the device's
121
- // environment gives: a recorder where a platform would be.
118
+ // environment gives: a recorder where a platform would be. `http.host` is
119
+ // the address the door binds, loopback when nothing says otherwise: a
120
+ // daemon on a café network is a harbor anyone on it can knock at, so the
121
+ // address is said by hand or it is 127.0.0.1.
122
122
  export type Options = { http?: { port: number; host?: string }; routes?: Routes; apps?: Apps | null; dial?: string[]; rings?: Record<string, Ring> };
123
123
 
124
124
  // The sockets live in the harbor's folder, where the folder's own permissions
@@ -139,17 +139,17 @@ export const sidePath = (dir: string) => socketPath(dir, 'side');
139
139
  // the first time only; to change one, remove the occupant and admit again.
140
140
  // The one path for a side on the device: agents, and sides on side.sock.
141
141
  //
142
- // The nonce is minted on the `local` faculty of this box's dock ward, in
142
+ // The nonce is minted on the `local` faculty of this box's box ward, in
143
143
  // this process, as a device fires its own timer. A box that lends no such
144
144
  // name has no local way in, and nothing here can make one up.
145
145
  export async function admit(harbor: Harbor, hosted: Hosted, identity: string, wake = false, reach = false): Promise<{ avatar?: Avatar; error?: string }> {
146
- const local = facultyOf<Local>(harbor, LOCAL);
146
+ const local = await lent<Local>(harbor, LOCAL);
147
147
  if (!local) return { error: 'this box has no local way in' };
148
148
  const found = await avatarOf(hosted, identity);
149
149
  if (!found.avatar) return found;
150
150
  const nonce = randomBytes(16).toString('hex');
151
151
  local.mint(nonce, { user: hosted.record.user, client: identity, ...(wake ? { wake: true } : {}), ...(reach ? { reach: true } : {}) });
152
- const entered = await found.avatar.enter({ ward: hosted.pk }, { kind: LOCAL, nonce });
152
+ const entered = await found.avatar.enter({ ward: hosted.pk }, { kind: 'local', nonce });
153
153
  local.drop(nonce);
154
154
  await hosted.save(); // the knock went through the ward's own door, which the harbor never sees
155
155
  if ('error' in entered && !('asks' in entered)) return { error: entered.error };
@@ -169,8 +169,8 @@ async function tick(harbor: DiskHarbor, now: number): Promise<void> {
169
169
  // booted a moment ago is still taking one, and a round that ran past her
170
170
  // would be a schedule placed and a minute missed for no reason a reader
171
171
  // could see.
172
- await settled();
173
- const timer = facultyOf<Timer>(harbor, TIMER);
172
+ await settled(harbor);
173
+ const timer = await lent<Timer>(harbor, TIMER);
174
174
  if (!timer) return;
175
175
  try {
176
176
  for (const woke of await timer.fire(now)) {
@@ -203,17 +203,55 @@ export async function serve(dir: string, options: Options = {}): Promise<Serving
203
203
  // world's own web origin; the push faculty reads the map at each ring, so
204
204
  // it is this daemon's `Push` body and no static set on a class.
205
205
  const rings: Record<string, Ring> = { ...options.rings };
206
+ // The web ring is hers alone: Web Push signed under the doorbell key she
207
+ // minted at the root's first ask, with this world's web origin as the
208
+ // subject a push service may complain to, known once the routes are.
209
+ let subject: string | undefined;
206
210
  class DaemonPush extends Push {
211
+ static override stands(): boolean {
212
+ return true;
213
+ }
214
+ #web: Ring | undefined;
207
215
  protected override get rings(): Readonly<Record<string, Ring>> {
208
- return rings;
216
+ const key = this.key;
217
+ if (rings.web || !key || !subject) return rings;
218
+ return { ...rings, web: (this.#web ??= webPush({ key, subject })) };
219
+ }
220
+ }
221
+ const harbor = new DiskHarbor(dir);
222
+ // The lines this box holds open are dialed for its own harbor, and an
223
+ // agent runs as an identity admitted on the local proof with a side over
224
+ // her avatar: the two live things only this process has, handed in the
225
+ // class bodies it brings.
226
+ class DaemonDialers extends Dialers {
227
+ static override stands(): boolean {
228
+ return true;
229
+ }
230
+ constructor(stance: Stance, box?: Harborage) {
231
+ super(stance, box, harbor);
232
+ }
233
+ }
234
+ class DaemonAgents extends Agents {
235
+ static override stands(): boolean {
236
+ return true;
237
+ }
238
+ constructor(stance: Stance, box?: Harborage) {
239
+ super(stance, box, async (row) => {
240
+ const hosted = harbor.wards.get(row.ward);
241
+ if (!hosted) throw new Error(`agent ${row.id}: this device hosts no ward ${row.ward}`);
242
+ const admitted = await admit(harbor, hosted, row.id);
243
+ if (!admitted.avatar) throw new Error(`agent ${row.id}: ${admitted.error}`);
244
+ // a process is the agent side, a model is the runner: both take a push as the event
245
+ return isProcess(row) ? agentSide(admitted.avatar, processRun(row.command, row.args, row.dir), () => hosted.save()) : runnerSide(admitted.avatar, row, () => {}, () => hosted.save());
246
+ });
209
247
  }
210
248
  }
211
- const harbor = new DiskHarbor(dir, undefined, { Push: DaemonPush, Grants, Agents, Dialers });
212
- // What a daemon lends beyond every box's faculties: `local`, the way in
213
- // for a process it saw on its own socket, and `grants`, the bearers its
214
- // routes honour. A device with no daemon has neither name, and the desk
215
- // there is a world nobody walks into locally.
216
- harbor.lent = { ...FACULTIES, [LOCAL]: 'Local', [GRANTS]: 'Grants', [AGENTS]: 'Agents', [DIALERS]: 'Dialers' };
249
+ // What a daemon brings to its own ward: the timer, `local`, the way in for
250
+ // a process it saw on its own socket, the grants its routes honour, and
251
+ // the ring, the lines and the agents it builds here. A device with no
252
+ // daemon brings none of the last five, and the desk there is a world
253
+ // nobody walks into locally.
254
+ harbor.faculties = { box: { Timer, Local, Grants, Http: Wire }, cli: { DaemonPush, DaemonDialers, DaemonAgents } };
217
255
  // A store that refused a save, once per ward. Every asker whose write was
218
256
  // refused has already been told, at the door that caused it, and none of
219
257
  // them is this operator: a disk that has stopped taking one world's writes
@@ -223,6 +261,14 @@ export async function serve(dir: string, options: Options = {}): Promise<Serving
223
261
  if (faults === 1) console.log(JSON.stringify({ at: new Date().toISOString(), refused: name }));
224
262
  };
225
263
  await harbor.boot();
264
+ // A contract two classes clashed under is lent to nobody, and the Dock
265
+ // keeps which; an operator reads it here, once, at boot, and the box is
266
+ // up either way.
267
+ const stood = (await harbor.wards.get(BOX_SEED)?.ask('ask', { being: BOX, method: 'lends' })) as { clashes?: Record<string, string[]> } | undefined;
268
+ for (const [word, keys] of Object.entries(stood?.clashes ?? {})) console.log(JSON.stringify({ at: new Date().toISOString(), clash: word, between: keys }));
269
+ // What the box is, asked of the Dock and read from no file: its routes,
270
+ // its apps and its default ward, each placed by the root through her asks.
271
+ const box = await boxOf(harbor);
226
272
  // A ward the folder keeps and the harbor could not open stops this daemon
227
273
  // here. The core is right to carry on without it, since a device with many
228
274
  // worlds must not lose them all to one, but a daemon's folder is an
@@ -250,7 +296,7 @@ export async function serve(dir: string, options: Options = {}): Promise<Serving
250
296
  let mcp: McpHttp | null = null;
251
297
  let agents: Agents | null = null;
252
298
  // the dialers faculty holds the lines; the route's view of them is hers
253
- const dialers = facultyOf<Dialers>(harbor, DIALERS);
299
+ const dialers = await lent<Dialers>(harbor, DIALERS);
254
300
  if (!dialers) throw new Error('this box has no dialers faculty');
255
301
  const quo: Quo = {
256
302
  sockets: new Set(),
@@ -278,37 +324,37 @@ export async function serve(dir: string, options: Options = {}): Promise<Serving
278
324
  // The box's grants, one faculty for every route: a bearer names an
279
325
  // identity in a world, the identity names her avatar there, and the
280
326
  // route only says the grammar. Nothing is minted for a bearer. Her
281
- // cells are the dock ward's to keep, so a token issued saves it.
282
- grants = facultyOf<Grants>(harbor, GRANTS) ?? null;
327
+ // cells are the box ward's to keep, so a token issued saves it.
328
+ grants = (await lent<Grants>(harbor, GRANTS)) ?? null;
283
329
  if (!grants) throw new Error('this box has no grants');
284
- grants.persist = () => harbor.wards.get(DOCK)?.save() ?? Promise.resolve();
285
- const resolve = async (identity: string, ward: string) => {
330
+ const findIn = async (identity: string, ward: string) => {
286
331
  const hosted = harbor.wards.get(ward);
287
332
  return hosted ? find(hosted, identity) : { error: 'no such world' };
288
333
  };
289
- const join = async (identity: string, invitation: Invitation, ward: string) => {
334
+ const joinIn = async (identity: string, invitation: Invitation, ward: string) => {
290
335
  const hosted = harbor.wards.get(ward);
291
336
  if (!hosted) return { error: 'no such world' };
292
337
  const r = await letIn(hosted, identity, invitation);
293
338
  return r.error ? { error: r.error } : {};
294
339
  };
295
- const routes = options.routes ?? (await readRoutes(harbor.dir));
340
+ const routes = options.routes ?? box.routes;
296
341
  const here = `http://${http.host}:${http.port}`;
297
342
  const at = { quo: routes?.quo ?? `${here}/quo`, web: routes?.web ?? `${here}/web` };
343
+ subject = new URL(at.web).origin;
298
344
  // a stranger's public being as {}, or an occupant's avatar under a
299
345
  // bearer; `web` is for its one human arrival, a redirect landing back
300
346
  // which ward a caller who names none is at is the terrain's word, and
301
347
  // on a disk it is the mark an operator wrote, read below at `defaultWard`
302
- http.mount('/api', apiRoute({ wards: harbor.wards, here: () => harbor.defaultWard }, { grants, resolve, join, web: at.web }));
348
+ http.mount('/api', apiRoute({ wards: harbor.wards, here: () => harbor.defaultWard }, { grants, resolve: findIn, join: joinIn }));
303
349
  // The web ring, under the world's own doorbell key: the push faculty
304
350
  // mints it the first time this box serves a web route and keeps it in
305
351
  // her cells, so it lives with the wards and in no file. The ring is
306
352
  // Web Push signed under it, and the subject a push service may
307
353
  // complain to is this world's own web route, which every world has.
308
- const push = facultyOf<Push>(harbor, PUSH);
354
+ const push = await lent<Push>(harbor, PUSH);
309
355
  if (!push) throw new Error('this box has no push faculty');
310
- const key = await push.mint();
311
- rings.web ??= webPush({ key, subject: new URL(at.web).origin });
356
+ const bell = (await harbor.wards.get(BOX_SEED)?.ask('ask', { being: await keyLent(harbor, PUSH), method: 'bell' })) as { bell?: string } | undefined;
357
+ if (typeof bell?.bell !== 'string') throw new Error('this box minted no doorbell key');
312
358
  // The two rings the stores own. A world holds no platform key and
313
359
  // never will, because Apple and Google sign a push only for the team
314
360
  // that published the app, so a ring to a phone is handed to a bell
@@ -331,17 +377,17 @@ export async function serve(dir: string, options: Options = {}): Promise<Serving
331
377
  // the worlds' pages: always, on the daemon's own door when no route names a public one
332
378
  // the bundles the dock's build emitted: read here, because reading a
333
379
  // file is the terrain's and a route is handed the code
334
- const web = webRoute(harbor, { at, code: await bundles(), folder: diskFolder(harbor.dir), apps: options.apps ?? (await readApps(harbor.dir)), bell: publicOf(key), shown: (n) => DiskHarbor.isShown(harbor.dir, n) });
380
+ const web = webRoute(harbor, { at, code: await bundles(), folder: diskFolder(harbor.dir), apps: options.apps ?? box.apps, bell: bell.bell, shown: (n) => DiskHarbor.isShown(harbor.dir, n) });
335
381
  if (routes) {
336
382
  // the worlds a client may be allowed into: every ward here but the
337
383
  // dock, which is the box's and nobody's world, by name and pk, the device's own first
338
384
  const worlds = () =>
339
385
  [...harbor.wards]
340
- .filter(([ward]) => ward !== DOCK)
386
+ .filter(([ward]) => ward !== BOX_SEED)
341
387
  .sort(([a], [b]) => (a === harbor.defaultWard ? -1 : b === harbor.defaultWard ? 1 : a.localeCompare(b)))
342
388
  .map(([ward, h]) => ({ ward, pk: h.pk, user: h.record.user }));
343
389
  // the MCP endpoint: a bearer names an identity in a world, the identity names her avatar there, the side runs beside her
344
- mcp = new McpHttp(resolve, async () => {
390
+ mcp = new McpHttp(findIn, async () => {
345
391
  for (const h of harbor.wards.values()) await h.save();
346
392
  });
347
393
  const route = await mcpRoute(danceOnDisk(harbor.dir), routes, mcp, grants);
@@ -350,7 +396,7 @@ export async function serve(dir: string, options: Options = {}): Promise<Serving
350
396
  http.mount('/.well-known', route.wellKnown); // the same discovery at the origin's root, where a client looks for an issuer that has a path
351
397
  const g = grants;
352
398
  mcp.gone = (identity, ward) => g.revoke(identity, ward); // removal at the ward ends the grant at the box
353
- exchange = new Exchange({ oauth, worlds, join });
399
+ exchange = new Exchange({ oauth, worlds, join: joinIn });
354
400
  }
355
401
  const ex = exchange;
356
402
  http.mount(
@@ -363,31 +409,25 @@ export async function serve(dir: string, options: Options = {}): Promise<Serving
363
409
  (ex && (await ex.handle(req, rest))) ??
364
410
  new Response('no such page', { status: 404, headers: { 'content-type': 'text/plain' } }),
365
411
  );
412
+ // Every other path is the box's wire: a faculty that claims it answers.
413
+ // Mounted last, so every route above is tried first.
414
+ const wire = await lent<Wire>(harbor, WIRE);
415
+ if (wire) http.mount('/', (req) => wire.handle(req));
366
416
  }
367
417
  // the routes this box holds a socket to are rows on the dialers faculty,
368
418
  // dialed from her cells at every boot; `dial` is a suite's way of placing one
369
419
  for (const url of options.dial ?? []) {
370
420
  const kept = dialers.keep({ url });
371
- if ('error' in kept) throw new Error(String(kept.error));
421
+ if ('error' in kept) throw new Error(JSON.stringify(kept.error));
372
422
  }
373
- dialers.start(harbor);
374
- // Which of its wards is the default: the agents and the ward the door
375
- // answers for hang off it, and a guess failing in silence is not a way
376
- // to find out.
377
- harbor.defaultWard = defaultWard(await DiskHarbor.marked(harbor.dir), [...harbor.wards.keys()]);
378
- // The agents faculty holds the table; this daemon is what runs a row:
379
- // the identity admitted into her ward on the local proof, and the agent
380
- // side over her avatar for as long as the daemon serves.
381
- agents = facultyOf<Agents>(harbor, AGENTS) ?? null;
423
+ // Which of its wards is the default: the ward the door answers for hangs
424
+ // off it, and a guess failing in silence is not a way to find out.
425
+ harbor.defaultWard = defaultWard(box.default === null ? [] : [box.default], [...harbor.wards.keys()]);
426
+ agents = (await lent<Agents>(harbor, AGENTS)) ?? null;
382
427
  if (!agents) throw new Error('this box has no agents faculty');
383
- await agents.start(async (row) => {
384
- const hosted = harbor.wards.get(row.ward);
385
- if (!hosted) throw new Error(`agent ${row.id}: this device hosts no ward ${row.ward}`);
386
- const admitted = await admit(harbor, hosted, row.id);
387
- if (!admitted.avatar) throw new Error(`agent ${row.id}: ${admitted.error}`);
388
- // a process is the agent side, a model is the runner: both take a push as the event
389
- return isProcess(row) ? agentSide(admitted.avatar, processRun(row.command, row.args, row.dir), () => hosted.save()) : runnerSide(admitted.avatar, row, () => {}, () => hosted.save());
390
- });
428
+ // The box is up: the lines are dialed and the agents run, and the daemon
429
+ // serves once they have.
430
+ await harbor.box.raise();
391
431
  } catch (e) {
392
432
  for (const s of servers) s.close();
393
433
  await agents?.close();
@@ -411,7 +451,7 @@ export async function serve(dir: string, options: Options = {}): Promise<Serving
411
451
  // daemon fire what is due right now.
412
452
  tick: (now: number) => tick(harbor, now),
413
453
  close: async () => {
414
- facultyOf<Timer>(harbor, TIMER)?.stop();
454
+ (await lent<Timer>(harbor, TIMER))?.stop();
415
455
  await mcp?.close();
416
456
  await agents?.close();
417
457
  dialers.close();
@@ -435,7 +475,7 @@ async function sideConnection(harbor: DiskHarbor, c: Socket): Promise<void> {
435
475
  const req = JSON.parse(hello) as { as?: string; ward?: string; run?: unknown };
436
476
  hosted = harbor.wards.get(req.ward ?? harbor.defaultWard);
437
477
  if (!hosted) throw new Error('no such ward');
438
- if (typeof req.as !== 'string' || !/^[\w.-]+$/.test(req.as) || req.as === hosted.record.user || req.as === 'desk') throw new Error('as is a word, and not a being of the ward');
478
+ if (!isIdentity(hosted, req.as)) throw new Error('as is a word, and not a being of the ward');
439
479
  as = req.as;
440
480
  if (req.run !== undefined) {
441
481
  const r = req.run as Partial<Model>;
@@ -527,55 +567,13 @@ async function handle(harbor: DiskHarbor, line: string): Promise<string> {
527
567
  }
528
568
  }
529
569
  const out = await hosted.ask(req.method, req.args ?? {});
530
- return JSON.stringify({ id, result: isSilence(out) ? { silence: true } : out });
570
+ // the ward it landed on, named back, since a request naming none is on the default and the caller may need its name
571
+ return JSON.stringify({ id, ward: hosted.name, result: isSilence(out) ? { silence: true } : out });
531
572
  } catch (e) {
532
573
  return JSON.stringify({ id, error: e instanceof Error ? e.message : String(e) });
533
574
  }
534
575
  }
535
576
 
536
- // The four routes by the one convention: `mcp.`, `web.`, `quo.` and `api.`
537
- // under one domain, over https. A domain is said once and the names follow;
538
- // an estate whose names break the convention edits the file it wrote.
539
- export const DOMAIN = /^[a-z0-9.-]+\.[a-z]{2,}$/i;
540
- export function routesFor(domain: string): Required<Routes> {
541
- if (!DOMAIN.test(domain)) throw new Error(`${domain} is not a domain`);
542
- return { mcp: `https://mcp.${domain}`, web: `https://web.${domain}`, quo: `https://quo.${domain}`, api: `https://api.${domain}` };
543
- }
544
- export async function writeRoutes(dir: string, domain: string): Promise<Required<Routes>> {
545
- const routes = routesFor(domain);
546
- await mkdir(dir, { recursive: true });
547
- await writeFile(join(dir, 'routes.json'), JSON.stringify(routes) + '\n');
548
- return routes;
549
- }
550
-
551
- // `<dir>/routes.json`: { mcp, web, quo?, api? }, the public origins the proxy serves.
552
- export async function readRoutes(dir: string): Promise<Routes | null> {
553
- const p = join(dir, 'routes.json');
554
- if (!existsSync(p)) return null;
555
- const r = JSON.parse(await readFile(p, 'utf8')) as Partial<Routes>;
556
- if (typeof r.mcp !== 'string' || typeof r.web !== 'string') return null;
557
- const out: Routes = { mcp: r.mcp.replace(/\/$/, ''), web: r.web.replace(/\/$/, '') };
558
- if (typeof r.quo === 'string') out.quo = r.quo.replace(/\/$/, '');
559
- if (typeof r.api === 'string') out.api = r.api.replace(/\/$/, '');
560
- return out;
561
- }
562
-
563
- // `<dir>/app.json`: { apple: [...], android: [{ package, fingerprints }] }, the
564
- // apps whose links this world's hostname opens, as the two platforms want
565
- // them named. An estate that ships no app of its own writes no such file,
566
- // serves no association file, and its links open the tab.
567
- export async function readApps(dir: string): Promise<Apps | null> {
568
- const p = join(dir, 'app.json');
569
- if (!existsSync(p)) return null;
570
- const r = JSON.parse(await readFile(p, 'utf8')) as Partial<Apps>;
571
- const apple = Array.isArray(r.apple) ? r.apple.filter((x): x is string => typeof x === 'string') : [];
572
- const android = Array.isArray(r.android)
573
- ? r.android
574
- .filter((a) => typeof a?.package === 'string' && Array.isArray(a.fingerprints))
575
- .map((a) => ({ package: a.package, fingerprints: a.fingerprints.filter((f): f is string => typeof f === 'string') }))
576
- : [];
577
- return apple.length === 0 && android.length === 0 ? null : { apple, android };
578
- }
579
577
 
580
578
 
581
579
 
package/cli/estate.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  // that device runs minus what it mints, its routes under the domain, no
5
5
  // agents, no schedules, no classes of its own, and that device's unit and
6
6
  // Caddyfile; and `mac/`, the launchd agent that runs the daemon on an
7
- // operator's Mac as a dialer, with the dial list it holds. A package file
7
+ // operator's Mac as a dialer. A package file
8
8
  // that depends on this dock and nothing else, and one document to fill in.
9
9
  // An estate needs nothing the dock does not give it, and this is the dock
10
10
  // giving it.
@@ -12,7 +12,7 @@ import { existsSync } from 'node:fs';
12
12
  import { mkdir, readFile, writeFile } from 'node:fs/promises';
13
13
  import { basename, join } from 'node:path';
14
14
  import { fileURLToPath } from 'node:url';
15
- import { DOMAIN, routesFor } from './daemon.ts';
15
+ import { DOMAIN } from '../box/index.ts';
16
16
 
17
17
  // This package's own version, read beside the emitted or the source tree.
18
18
  async function version(): Promise<string> {
@@ -33,7 +33,6 @@ export async function estate(dir: string, domain: string): Promise<{ dir: string
33
33
  const files: Record<string, string> = {
34
34
  'package.json': JSON.stringify({ name, private: true, description: `The ${name} estate: one owner's harbors, built on the dock and nothing else.`, type: 'module', dependencies: { '@nervur-org/dock': `^${await version()}` } }, null, 2) + '\n',
35
35
  [`${name}.md`]: `# ${name}\n\nAn estate: all of one owner's harbors. One folder per harbor, each the harbor\ndirectory that device runs minus what it mints, plus that device's unit.\n\`droplet/\` is the first: its routes under \`${domain}\`, its agents, its\nclasses, its systemd unit and its Caddyfile. This folder depends on the\ndock and nothing else.\n\n## The droplet\n\nWhere it is, how it is reached, and what was done to stand it up: yours\nto write.\n`,
36
- 'droplet/routes.json': JSON.stringify(routesFor(domain)) + '\n',
37
36
  'droplet/stand.ts': `// SPDX-License-Identifier: Apache-2.0\n// What the root stands up on this harbor, once, after the inits and with no\n// daemon over the folder: the beings the estate boots for its own working\n// and the relations between them, placed on real invitations. Run as\n// \`node droplet/stand.ts <dir>\`; twice is once.\nimport { stand } from '@nervur-org/dock/stand';\n\nconsole.log(await stand(process.argv[2] ?? process.env.NERVUR_DIR ?? '', { boot: [], wire: [] }));\n`,
38
37
  'droplet/classes/index.ts': '// The classes this harbor holds beside the dock\'s built-in ones, exported\n// by name.\nexport {};\n',
39
38
  'droplet/nervur.service': (await template('nervur.service')).replaceAll('ESTATE', name),