@nervur-org/dock 0.3.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 (285) hide show
  1. package/GETTING_STARTED.md +128 -0
  2. package/LICENSE +202 -0
  3. package/NOTICE +6 -0
  4. package/README.md +37 -0
  5. package/api/quo-api.md +196 -0
  6. package/api/route.ts +227 -0
  7. package/beings/GLOSSARY.md +118 -0
  8. package/beings/WORLDS.md +567 -0
  9. package/beings/avatar.ts +101 -0
  10. package/beings/carry.ts +159 -0
  11. package/beings/clock.ts +552 -0
  12. package/beings/courier.ts +107 -0
  13. package/beings/desk.ts +128 -0
  14. package/beings/doorbell.ts +129 -0
  15. package/beings/envoy.ts +218 -0
  16. package/beings/index.ts +16 -0
  17. package/beings/join.ts +51 -0
  18. package/beings/link.ts +104 -0
  19. package/beings/look.ts +139 -0
  20. package/beings/porter.ts +43 -0
  21. package/beings/presence.ts +39 -0
  22. package/beings/quo-app-bells.md +280 -0
  23. package/beings/quo-dock.md +1942 -0
  24. package/beings/setup.ts +45 -0
  25. package/beings/side.ts +60 -0
  26. package/beings/user.ts +459 -0
  27. package/beings/way.ts +65 -0
  28. package/cli/bundles.ts +20 -0
  29. package/cli/client.ts +73 -0
  30. package/cli/daemon.ts +581 -0
  31. package/cli/estate/Caddyfile +33 -0
  32. package/cli/estate/quo.service +35 -0
  33. package/cli/estate/systems.quo.plist +44 -0
  34. package/cli/estate.ts +48 -0
  35. package/cli/folder.ts +27 -0
  36. package/cli/oauth.ts +32 -0
  37. package/cli/pilot.ts +38 -0
  38. package/cli/quo-dock.md +1942 -0
  39. package/cli/quo.ts +203 -0
  40. package/cli/stand.ts +160 -0
  41. package/dist/api/route.d.ts +21 -0
  42. package/dist/api/route.js +193 -0
  43. package/dist/beings/avatar.d.ts +32 -0
  44. package/dist/beings/avatar.js +100 -0
  45. package/dist/beings/carry.d.ts +12 -0
  46. package/dist/beings/carry.js +174 -0
  47. package/dist/beings/clock.d.ts +161 -0
  48. package/dist/beings/clock.js +528 -0
  49. package/dist/beings/courier.d.ts +50 -0
  50. package/dist/beings/courier.js +96 -0
  51. package/dist/beings/desk.d.ts +53 -0
  52. package/dist/beings/desk.js +135 -0
  53. package/dist/beings/doorbell.d.ts +78 -0
  54. package/dist/beings/doorbell.js +133 -0
  55. package/dist/beings/envoy.d.ts +54 -0
  56. package/dist/beings/envoy.js +211 -0
  57. package/dist/beings/index.d.ts +14 -0
  58. package/dist/beings/index.js +16 -0
  59. package/dist/beings/join.d.ts +15 -0
  60. package/dist/beings/join.js +42 -0
  61. package/dist/beings/link.d.ts +14 -0
  62. package/dist/beings/link.js +91 -0
  63. package/dist/beings/look.d.ts +30 -0
  64. package/dist/beings/look.js +104 -0
  65. package/dist/beings/porter.d.ts +14 -0
  66. package/dist/beings/porter.js +41 -0
  67. package/dist/beings/presence.d.ts +8 -0
  68. package/dist/beings/presence.js +21 -0
  69. package/dist/beings/setup.d.ts +3 -0
  70. package/dist/beings/setup.js +39 -0
  71. package/dist/beings/side.d.ts +32 -0
  72. package/dist/beings/side.js +37 -0
  73. package/dist/beings/user.d.ts +315 -0
  74. package/dist/beings/user.js +462 -0
  75. package/dist/beings/way.d.ts +19 -0
  76. package/dist/beings/way.js +28 -0
  77. package/dist/cli/bundles.d.ts +5 -0
  78. package/dist/cli/bundles.js +17 -0
  79. package/dist/cli/client.d.ts +15 -0
  80. package/dist/cli/client.js +58 -0
  81. package/dist/cli/daemon.d.ts +50 -0
  82. package/dist/cli/daemon.js +584 -0
  83. package/dist/cli/estate/Caddyfile +33 -0
  84. package/dist/cli/estate/quo.service +35 -0
  85. package/dist/cli/estate/systems.quo.plist +44 -0
  86. package/dist/cli/estate.d.ts +5 -0
  87. package/dist/cli/estate.js +50 -0
  88. package/dist/cli/folder.d.ts +2 -0
  89. package/dist/cli/folder.js +25 -0
  90. package/dist/cli/oauth.d.ts +2 -0
  91. package/dist/cli/oauth.js +32 -0
  92. package/dist/cli/pilot.d.ts +5 -0
  93. package/dist/cli/pilot.js +22 -0
  94. package/dist/cli/quo.d.ts +2 -0
  95. package/dist/cli/quo.js +201 -0
  96. package/dist/cli/stand.d.ts +37 -0
  97. package/dist/cli/stand.js +110 -0
  98. package/dist/dock/agents.d.ts +102 -0
  99. package/dist/dock/agents.js +105 -0
  100. package/dist/dock/apns.d.ts +27 -0
  101. package/dist/dock/apns.js +98 -0
  102. package/dist/dock/console.d.ts +6 -0
  103. package/dist/dock/console.js +20 -0
  104. package/dist/dock/dialers.d.ts +56 -0
  105. package/dist/dock/dialers.js +75 -0
  106. package/dist/dock/faculty.d.ts +24 -0
  107. package/dist/dock/faculty.js +142 -0
  108. package/dist/dock/fcm.d.ts +13 -0
  109. package/dist/dock/fcm.js +77 -0
  110. package/dist/dock/grants.d.ts +41 -0
  111. package/dist/dock/grants.js +99 -0
  112. package/dist/dock/http2.d.ts +4 -0
  113. package/dist/dock/http2.js +68 -0
  114. package/dist/dock/index.d.ts +41 -0
  115. package/dist/dock/index.js +113 -0
  116. package/dist/dock/local.d.ts +13 -0
  117. package/dist/dock/local.js +44 -0
  118. package/dist/dock/location.d.ts +19 -0
  119. package/dist/dock/location.js +11 -0
  120. package/dist/dock/notifications.d.ts +38 -0
  121. package/dist/dock/notifications.js +60 -0
  122. package/dist/dock/push.d.ts +79 -0
  123. package/dist/dock/push.js +132 -0
  124. package/dist/dock/service.d.ts +4 -0
  125. package/dist/dock/service.js +107 -0
  126. package/dist/dock/timer.d.ts +52 -0
  127. package/dist/dock/timer.js +149 -0
  128. package/dist/dock/webpush.d.ts +12 -0
  129. package/dist/dock/webpush.js +144 -0
  130. package/dist/harbor/browser.d.ts +15 -0
  131. package/dist/harbor/browser.js +82 -0
  132. package/dist/harbor/capacitor.d.ts +28 -0
  133. package/dist/harbor/capacitor.js +218 -0
  134. package/dist/harbor/disk.d.ts +27 -0
  135. package/dist/harbor/disk.js +249 -0
  136. package/dist/harbor/edge/alarm.d.ts +10 -0
  137. package/dist/harbor/edge/alarm.js +38 -0
  138. package/dist/harbor/edge/edge.d.ts +54 -0
  139. package/dist/harbor/edge/edge.js +370 -0
  140. package/dist/harbor/edge/exercise.d.ts +7 -0
  141. package/dist/harbor/edge/exercise.js +232 -0
  142. package/dist/harbor/edge/given.d.ts +26 -0
  143. package/dist/harbor/edge/given.js +42 -0
  144. package/dist/harbor/edge/platform.d.ts +39 -0
  145. package/dist/harbor/edge/storage.d.ts +30 -0
  146. package/dist/harbor/edge/storage.js +180 -0
  147. package/dist/harbor/edge/worker.d.ts +17 -0
  148. package/dist/harbor/edge/worker.js +67 -0
  149. package/dist/harbor/files.d.ts +15 -0
  150. package/dist/harbor/files.js +220 -0
  151. package/dist/harbor/http.d.ts +19 -0
  152. package/dist/harbor/http.js +177 -0
  153. package/dist/harbor/idb.d.ts +15 -0
  154. package/dist/harbor/idb.js +0 -0
  155. package/dist/harbor/quo.d.ts +10 -0
  156. package/dist/harbor/quo.js +42 -0
  157. package/dist/harbor/resolve.d.ts +16 -0
  158. package/dist/harbor/resolve.js +17 -0
  159. package/dist/harbor/seal.d.ts +3 -0
  160. package/dist/harbor/seal.js +25 -0
  161. package/dist/harbor/sealed.d.ts +35 -0
  162. package/dist/harbor/sealed.js +162 -0
  163. package/dist/harbor/tauri.d.ts +16 -0
  164. package/dist/harbor/tauri.js +59 -0
  165. package/dist/harbor/ward.d.ts +9 -0
  166. package/dist/harbor/ward.js +15 -0
  167. package/dist/human/app.d.ts +32 -0
  168. package/dist/human/app.js +527 -0
  169. package/dist/human/apps.d.ts +10 -0
  170. package/dist/human/apps.js +29 -0
  171. package/dist/human/dom.d.ts +2 -0
  172. package/dist/human/dom.js +15 -0
  173. package/dist/human/door.d.ts +19 -0
  174. package/dist/human/door.js +92 -0
  175. package/dist/human/guest.d.ts +3 -0
  176. package/dist/human/guest.js +33 -0
  177. package/dist/human/html.d.ts +55 -0
  178. package/dist/human/html.js +451 -0
  179. package/dist/human/local.d.ts +7 -0
  180. package/dist/human/local.js +10 -0
  181. package/dist/human/mark.d.ts +8 -0
  182. package/dist/human/mark.js +47 -0
  183. package/dist/human/move.d.ts +17 -0
  184. package/dist/human/move.js +90 -0
  185. package/dist/human/screen.d.ts +19 -0
  186. package/dist/human/screen.js +119 -0
  187. package/dist/human/style.d.ts +2 -0
  188. package/dist/human/style.js +60 -0
  189. package/dist/human/tab.bundle.txt +4775 -0
  190. package/dist/human/tab.d.ts +15 -0
  191. package/dist/human/tab.js +409 -0
  192. package/dist/human/tree.d.ts +50 -0
  193. package/dist/human/tree.js +157 -0
  194. package/dist/human/wake.d.ts +34 -0
  195. package/dist/human/wake.js +179 -0
  196. package/dist/human/web.d.ts +33 -0
  197. package/dist/human/web.js +184 -0
  198. package/dist/human/worker.bundle.txt +3868 -0
  199. package/dist/human/worker.d.ts +1 -0
  200. package/dist/human/worker.js +81 -0
  201. package/dist/human/worlds.d.ts +24 -0
  202. package/dist/human/worlds.js +99 -0
  203. package/dist/mcp/agent.d.ts +10 -0
  204. package/dist/mcp/agent.js +78 -0
  205. package/dist/mcp/allow.d.ts +20 -0
  206. package/dist/mcp/allow.js +88 -0
  207. package/dist/mcp/http.d.ts +27 -0
  208. package/dist/mcp/http.js +92 -0
  209. package/dist/mcp/oauth.d.ts +91 -0
  210. package/dist/mcp/oauth.js +241 -0
  211. package/dist/mcp/route.d.ts +15 -0
  212. package/dist/mcp/route.js +42 -0
  213. package/dist/mcp/runner.d.ts +45 -0
  214. package/dist/mcp/runner.js +99 -0
  215. package/dist/mcp/server.d.ts +11 -0
  216. package/dist/mcp/server.js +102 -0
  217. package/dock/agents.ts +121 -0
  218. package/dock/apns.ts +138 -0
  219. package/dock/console.ts +23 -0
  220. package/dock/dialers.ts +85 -0
  221. package/dock/faculty.ts +148 -0
  222. package/dock/fcm.ts +103 -0
  223. package/dock/grants.ts +111 -0
  224. package/dock/http2.ts +70 -0
  225. package/dock/index.ts +133 -0
  226. package/dock/local.ts +48 -0
  227. package/dock/location.ts +22 -0
  228. package/dock/notifications.ts +67 -0
  229. package/dock/push.ts +161 -0
  230. package/dock/quo-app-bells.md +280 -0
  231. package/dock/quo-dock.md +1942 -0
  232. package/dock/service.ts +100 -0
  233. package/dock/timer.ts +174 -0
  234. package/dock/webpush.ts +182 -0
  235. package/harbor/browser.ts +97 -0
  236. package/harbor/capacitor.ts +223 -0
  237. package/harbor/disk.ts +237 -0
  238. package/harbor/edge/alarm.ts +48 -0
  239. package/harbor/edge/edge.ts +407 -0
  240. package/harbor/edge/exercise.ts +241 -0
  241. package/harbor/edge/given.ts +106 -0
  242. package/harbor/edge/platform.d.ts +39 -0
  243. package/harbor/edge/storage.ts +179 -0
  244. package/harbor/edge/text.d.ts +15 -0
  245. package/harbor/edge/worker.ts +70 -0
  246. package/harbor/edge/wrangler.toml +30 -0
  247. package/harbor/files.ts +221 -0
  248. package/harbor/http.ts +188 -0
  249. package/harbor/idb.ts +0 -0
  250. package/harbor/quo-harbor.md +850 -0
  251. package/harbor/quo.ts +44 -0
  252. package/harbor/resolve.ts +31 -0
  253. package/harbor/seal.ts +26 -0
  254. package/harbor/sealed.ts +183 -0
  255. package/harbor/tauri.ts +79 -0
  256. package/harbor/ward.ts +28 -0
  257. package/human/app.ts +580 -0
  258. package/human/apps.ts +47 -0
  259. package/human/dom.ts +21 -0
  260. package/human/door.ts +113 -0
  261. package/human/guest.ts +34 -0
  262. package/human/html.ts +490 -0
  263. package/human/local.ts +30 -0
  264. package/human/mark.ts +53 -0
  265. package/human/move.ts +122 -0
  266. package/human/quo-human.md +1042 -0
  267. package/human/screen.ts +136 -0
  268. package/human/style.ts +63 -0
  269. package/human/tab.bundle.txt +4775 -0
  270. package/human/tab.ts +446 -0
  271. package/human/tree.ts +217 -0
  272. package/human/wake.ts +191 -0
  273. package/human/web.ts +259 -0
  274. package/human/worker.bundle.txt +3868 -0
  275. package/human/worker.ts +124 -0
  276. package/human/worlds.ts +127 -0
  277. package/mcp/agent.ts +94 -0
  278. package/mcp/allow.ts +131 -0
  279. package/mcp/http.ts +92 -0
  280. package/mcp/oauth.ts +270 -0
  281. package/mcp/quo-mcp.md +343 -0
  282. package/mcp/route.ts +45 -0
  283. package/mcp/runner.ts +136 -0
  284. package/mcp/server.ts +101 -0
  285. package/package.json +121 -0
@@ -0,0 +1,50 @@
1
+ import { DiskHarbor, type Hosted } from '../harbor/disk.ts';
2
+ import type { Harbor } from '@nervur-org/nervur/harbor';
3
+ import type { Avatar } from '../beings/index.ts';
4
+ export { avatarOf, letIn, find } from '../beings/join.ts';
5
+ import { type Ring } from '../dock/index.ts';
6
+ import { Grants } from '../dock/grants.ts';
7
+ import { Agents } from '../dock/agents.ts';
8
+ import type { OAuth } from '../mcp/oauth.ts';
9
+ import { Exchange } from '../mcp/allow.ts';
10
+ import { McpHttp } from '../mcp/http.ts';
11
+ import { type Routes } from '../mcp/route.ts';
12
+ import { type Apps } from '../human/web.ts';
13
+ import { type Http, type Quo } from '../harbor/http.ts';
14
+ export type { Routes } from '../mcp/route.ts';
15
+ export type Serving = {
16
+ harbor: DiskHarbor;
17
+ sock: string;
18
+ side: string;
19
+ http: Http | null;
20
+ grants: Grants | null;
21
+ oauth: OAuth | null;
22
+ exchange: Exchange | null;
23
+ mcp: McpHttp | null;
24
+ agents: Agents | null;
25
+ quo: Quo;
26
+ tick(now: number): Promise<void>;
27
+ close(): Promise<void>;
28
+ };
29
+ export type Options = {
30
+ http?: {
31
+ port: number;
32
+ host?: string;
33
+ };
34
+ routes?: Routes;
35
+ apps?: Apps | null;
36
+ dial?: string[];
37
+ rings?: Record<string, Ring>;
38
+ };
39
+ export declare const sockPath: (dir: string) => string;
40
+ export declare const sidePath: (dir: string) => string;
41
+ export declare function admit(harbor: Harbor, hosted: Hosted, identity: string, wake?: boolean, reach?: boolean): Promise<{
42
+ avatar?: Avatar;
43
+ error?: string;
44
+ }>;
45
+ export declare function serve(dir: string, options?: Options): Promise<Serving>;
46
+ export declare const DOMAIN: RegExp;
47
+ export declare function routesFor(domain: string): Required<Routes>;
48
+ export declare function writeRoutes(dir: string, domain: string): Promise<Required<Routes>>;
49
+ export declare function readRoutes(dir: string): Promise<Routes | null>;
50
+ export declare function readApps(dir: string): Promise<Apps | null>;
@@ -0,0 +1,584 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // The daemon: `quo serve`. One process holds the harbor and the ask pointer
3
+ // of every ward in it. Two local doors, never mixed, each a unix socket only
4
+ // the device's user can read:
5
+ //
6
+ // quo.sock the root. One line of JSON in, one line out.
7
+ // -> { id, ward?, method?, args?, via? }
8
+ // <- { id, result } | { id, error }
9
+ // With `being`, the line is the owner's `ask`, forwarded like
10
+ // the other six: the ward names the asker, bounds the wait and
11
+ // judges her answer, and the pointer saves before it returns.
12
+ // -> { id, ward?, being, method?, args? }
13
+ // A result of silence crosses as { silence: true }, because the
14
+ // wire has no symbol. With `via`, the line is an owner ask on
15
+ // another ward: `via` names a standing the user being holds
16
+ // at that ward, and the daemon asks on it in-process, as it
17
+ // drives an avatar for a side. The answer is what the far
18
+ // ward says to an owner at its door.
19
+ // side.sock local sides. The first line names the client identity, and
20
+ // every line after is MCP, JSON-RPC, one message per line. The
21
+ // daemon runs the model side in-process with the avatar; the
22
+ // process on the other end of the socket is a pipe to stdio.
23
+ // -> { as, ward? }
24
+ // <- { ok: as } | { error }
25
+ // With `run`, the side is the runner instead: a model at a
26
+ // completions endpoint, driven from here. Every line after the
27
+ // hello is one line from the human and starts a turn; every
28
+ // final text the model says comes back as one line, whoever
29
+ // started the turn. The bearer, if any, is read from the
30
+ // device by the client and crosses only this socket.
31
+ // -> { as, ward?, run: { url, model, key?, turns? } }
32
+ // <- { ok: as } | { error }, then one line per final text
33
+ //
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
36
+ // box's own way in, and the desk trades it at a standing there, once. A
37
+ // `local` proof arriving any other way names no nonce and is refused.
38
+ //
39
+ // Agents have no door: the `agents` faculty of the dock ward holds each
40
+ // agent's identity, her ward, her command and her folder, placed by the
41
+ // root with `keep` as a schedule is placed on the clock, and the daemon
42
+ // admits her on the local proof and runs the agent side beside her avatar
43
+ // for as long as it serves. A push to her from the user being runs the
44
+ // command in the folder with the event.
45
+ //
46
+ // One timer on the box, and she is the `timer` faculty of its dock: the only
47
+ // thing on the device that reads its clock. She arms herself for the next
48
+ // moment any Clock who lent her is due, and for nothing when none is, and
49
+ // the daemon winds nothing; what it holds is `tick`, a round at a moment
50
+ // somebody names, for a suite and an operator. A tick wakes every being
51
+ // that asked to be woken, the Clock among them, at her own door as the
52
+ // occupant `timer`, and she wakes everyone else through their doors, on
53
+ // standings the root placed, so every wake is named, counted, once-only and
54
+ // judged at a gate. What she holds is in her cells, placed by the root with
55
+ // `keep`, and the daemon reads no schedule from anywhere. A daemon with no
56
+ // clock booted holds a timer that finds nobody and arms nothing.
57
+ //
58
+ // The dock is built here and handed to the harbor, once: the timer, and the
59
+ // push faculty with one ring per token kind this device can ring.
60
+ //
61
+ // The default ward is the one a caller means when naming none: the one ward
62
+ // hosted, or the one marked in its own folder by `quo default`. A harbor is
63
+ // not a being and has no self to be, so the one thing it can do is point at
64
+ // one of its wards. Its agents boot there and its door answers for it when a
65
+ // caller names no ward. Several wards and no mark is refused here, by name,
66
+ // rather than coming up on a guess. What stands in a ward is the ward's own
67
+ // truth, restored from its folder, and the daemon reads no recipe at boot.
68
+ //
69
+ // A third door, for occupants from elsewhere, is one HTTP listener on
70
+ // localhost that a reverse proxy fronts, `harbor/http.ts`. Routes mount on it by
71
+ // path, and each lives in the folder whose truth it is: `/web`, the worlds'
72
+ // pages and the allow page, in `human/web.ts` and `mcp/allow.ts`; `/mcp`, the
73
+ // model side behind its credential exchange, in `mcp/route.ts`; `/quo`, the
74
+ // socket door, in `harbor/quo.ts`; `/api`, a public being's describe and
75
+ // asks as plain JSON for anyone, in `api/route.ts`. It listens only when asked, and only on
76
+ // loopback: the proxy is what faces the world, and the daemon never does.
77
+ // With a row on the dialers faculty, placed by the root with `keep`, this
78
+ // daemon is itself a dialer: it holds one socket to each URL, announces its
79
+ // wards, and reconnects with backoff, so a Mac behind NAT is reached
80
+ // through the droplet it dialed.
81
+ import { createServer } from 'node:net';
82
+ import { unlink, chmod, mkdir, readFile, writeFile } from 'node:fs/promises';
83
+ import { existsSync } from 'node:fs';
84
+ import { createHash, randomBytes } from 'node:crypto';
85
+ import { tmpdir } from 'node:os';
86
+ import { join, resolve } from 'node:path';
87
+ import { createInterface } from 'node:readline';
88
+ import { isSilence, isWord, wordOf } from '@nervur-org/nervur';
89
+ import { DiskHarbor, defaultWard } from '../harbor/disk.js';
90
+ // an identity in a world: the three asks any terrain's routes make at a ward
91
+ export { avatarOf, letIn, find } from '../beings/join.js';
92
+ import { avatarOf, letIn, find } from '../beings/join.js';
93
+ import { DOCK, FACULTIES, LOCAL, PUSH, TIMER, Local, Push, Timer, facultyOf, publicOf, routed, settled, webPush } from '../dock/index.js';
94
+ import { apnsHttp2, apnsOf } from '../dock/http2.js';
95
+ import { GRANTS, Grants } from '../dock/grants.js';
96
+ import { AGENTS, Agents, isProcess } from '../dock/agents.js';
97
+ import { DIALERS, Dialers } from '../dock/dialers.js';
98
+ import { mcpSide } from '../mcp/server.js';
99
+ import { runnerSide } from '../mcp/runner.js';
100
+ import { agentSide, processRun } from '../mcp/agent.js';
101
+ import { Exchange } from '../mcp/allow.js';
102
+ import { McpHttp } from '../mcp/http.js';
103
+ import { mcpRoute } from '../mcp/route.js';
104
+ import { webRoute } from '../human/web.js';
105
+ import { bundles } from './bundles.js';
106
+ import { diskFolder } from './folder.js';
107
+ import { danceOnDisk } from './oauth.js';
108
+ import { quoRoute } from '../harbor/quo.js';
109
+ import { apiRoute } from '../api/route.js';
110
+ import { listenHttp } from '../harbor/http.js';
111
+ // The sockets live in the harbor's folder, where the folder's own permissions
112
+ // guard them. A unix socket path is short by law, about a hundred bytes on
113
+ // macOS, so a deep folder gets its sockets in the temp dir under a name only
114
+ // its path produces. Client and daemon compute the same answer.
115
+ const socketPath = (dir, name) => {
116
+ const inside = join(resolve(dir), `${name}.sock`);
117
+ if (inside.length < 100)
118
+ return inside;
119
+ return join(tmpdir(), `quo-${createHash('sha256').update(resolve(dir)).digest('hex').slice(0, 16)}-${name}.sock`);
120
+ };
121
+ export const sockPath = (dir) => socketPath(dir, 'quo');
122
+ export const sidePath = (dir) => socketPath(dir, 'side');
123
+ // Admit a local client identity into a ward: find or boot her avatar, and
124
+ // enter her with a nonce the box's way in honours once. On a reconnect she
125
+ // already holds `user` and nothing is minted, so `wake` and `reach` are read
126
+ // the first time only; to change one, remove the occupant and admit again.
127
+ // The one path for a side on the device: agents, and sides on side.sock.
128
+ //
129
+ // The nonce is minted on the `local` faculty of this box's dock ward, in
130
+ // this process, as a device fires its own timer. A box that lends no such
131
+ // name has no local way in, and nothing here can make one up.
132
+ export async function admit(harbor, hosted, identity, wake = false, reach = false) {
133
+ const local = facultyOf(harbor, LOCAL);
134
+ if (!local)
135
+ return { error: 'this box has no local way in' };
136
+ const found = await avatarOf(hosted, identity);
137
+ if (!found.avatar)
138
+ return found;
139
+ const nonce = randomBytes(16).toString('hex');
140
+ local.mint(nonce, { user: hosted.record.user, client: identity, ...(wake ? { wake: true } : {}), ...(reach ? { reach: true } : {}) });
141
+ const entered = await found.avatar.enter({ ward: hosted.pk }, { kind: LOCAL, nonce });
142
+ local.drop(nonce);
143
+ await hosted.save(); // the knock went through the ward's own door, which the harbor never sees
144
+ if ('error' in entered && !('asks' in entered))
145
+ return { error: entered.error };
146
+ return { avatar: found.avatar };
147
+ }
148
+ // One round of the box's timer. She is a being of the harbor's own ward and
149
+ // arms her own round at birth, so nothing here winds her; this is the round
150
+ // at a moment somebody names, which is how a suite proves a wake without
151
+ // waiting a minute for one and how an operator fires what is due now.
152
+ //
153
+ // Every being who invited her is asked at her own door, as the occupant she
154
+ // minted, with the moment in the args. A box whose timer nobody invited
155
+ // wakes nobody.
156
+ async function tick(harbor, now) {
157
+ // Whoever was born since the last round takes her standing first. A being
158
+ // booted a moment ago is still taking one, and a round that ran past her
159
+ // would be a schedule placed and a minute missed for no reason a reader
160
+ // could see.
161
+ await settled();
162
+ const timer = facultyOf(harbor, TIMER);
163
+ if (!timer)
164
+ return;
165
+ try {
166
+ for (const woke of await timer.fire(now)) {
167
+ const out = woke.out;
168
+ // The default clock keeps no fire log: the daemon's own output is the
169
+ // record, and the box that would read it is the box the work ran on. A
170
+ // minute with nothing due says nothing, so a journal shows the wakes and
171
+ // not the clock.
172
+ if (out && typeof out === 'object' && Array.isArray(out.fired) && out.fired.length)
173
+ console.log(JSON.stringify({ tick: new Date(now).toISOString(), fired: out.fired }));
174
+ }
175
+ }
176
+ finally {
177
+ for (const h of harbor.wards.values())
178
+ await h.save(); // the wakes went through the wards' own doors, which the harbor never sees
179
+ }
180
+ }
181
+ async function listen(path, onConnection) {
182
+ await unlink(path).catch(() => { }); // a socket left by a process that died holds nobody
183
+ const server = createServer(onConnection);
184
+ await new Promise((ok, no) => {
185
+ server.once('error', no);
186
+ server.listen(path, () => ok());
187
+ });
188
+ await chmod(path, 0o600);
189
+ return server;
190
+ }
191
+ export async function serve(dir, options = {}) {
192
+ // The box's own ward, before any other boots on it. The rings are filled
193
+ // in below once the routes are known, since Web Push signs under this
194
+ // world's own web origin; the push faculty reads the map at each ring, so
195
+ // it is this daemon's `Push` body and no static set on a class.
196
+ const rings = { ...options.rings };
197
+ class DaemonPush extends Push {
198
+ get rings() {
199
+ return rings;
200
+ }
201
+ }
202
+ const harbor = new DiskHarbor(dir, undefined, { Push: DaemonPush, Grants, Agents, Dialers });
203
+ // What a daemon lends beyond every box's faculties: `local`, the way in
204
+ // for a process it saw on its own socket, and `grants`, the bearers its
205
+ // routes honour. A device with no daemon has neither name, and the desk
206
+ // there is a world nobody walks into locally.
207
+ harbor.lent = { ...FACULTIES, [LOCAL]: 'Local', [GRANTS]: 'Grants', [AGENTS]: 'Agents', [DIALERS]: 'Dialers' };
208
+ // A store that refused a save, once per ward. Every asker whose write was
209
+ // refused has already been told, at the door that caused it, and none of
210
+ // them is this operator: a disk that has stopped taking one world's writes
211
+ // is the thing the journal is for. Once, because a full disk refuses every
212
+ // save after the first and a line per ask is a log nobody reads.
213
+ harbor.onFault = (name, faults) => {
214
+ if (faults === 1)
215
+ console.log(JSON.stringify({ at: new Date().toISOString(), refused: name }));
216
+ };
217
+ await harbor.boot();
218
+ // A ward the folder keeps and the harbor could not open stops this daemon
219
+ // here. The core is right to carry on without it, since a device with many
220
+ // worlds must not lose them all to one, but a daemon's folder is an
221
+ // operator's and every ward in it is meant to be served: coming up quietly
222
+ // with a world missing is that world gone dark with nobody told, and the
223
+ // form refusal the files store makes, a sealed folder and no key, would be
224
+ // a daemon serving nothing at all instead of saying so. The reason is the
225
+ // store's own, asked for again since the boot has only the name.
226
+ if (harbor.unbooted.length) {
227
+ const name = harbor.unbooted[0];
228
+ const why = await harbor.store.load(name).then(() => 'it would not boot', (e) => (e instanceof Error ? e.message : String(e)));
229
+ await harbor.close();
230
+ throw new Error(`the ward ${name} in ${harbor.dir}: ${why}`);
231
+ }
232
+ const sock = sockPath(harbor.dir);
233
+ const side = sidePath(harbor.dir);
234
+ const servers = [];
235
+ let http = null;
236
+ let grants = null;
237
+ let oauth = null;
238
+ let exchange = null;
239
+ let mcp = null;
240
+ let agents = null;
241
+ // the dialers faculty holds the lines; the route's view of them is hers
242
+ const dialers = facultyOf(harbor, DIALERS);
243
+ if (!dialers)
244
+ throw new Error('this box has no dialers faculty');
245
+ const quo = {
246
+ sockets: new Set(),
247
+ get dialers() {
248
+ return [...dialers.open.values()];
249
+ },
250
+ };
251
+ try {
252
+ servers.push(await listen(sock, (c) => {
253
+ createInterface({ input: c }).on('line', (line) => {
254
+ void handle(harbor, line).then((reply) => c.write(reply + '\n'));
255
+ });
256
+ c.on('error', () => { });
257
+ }));
258
+ servers.push(await listen(side, (c) => void sideConnection(harbor, c)));
259
+ if (options.http) {
260
+ const { server, ...door } = await listenHttp(harbor, options.http.host ?? '127.0.0.1', options.http.port, quo);
261
+ servers.push(server);
262
+ http = door;
263
+ // the sealed bytes; the sockets a dialer opens are held by the door
264
+ // itself, which is where this terrain's listener half lives
265
+ http.mount('/quo', quoRoute(harbor));
266
+ // The box's grants, one faculty for every route: a bearer names an
267
+ // identity in a world, the identity names her avatar there, and the
268
+ // route only says the grammar. Nothing is minted for a bearer. Her
269
+ // cells are the dock ward's to keep, so a token issued saves it.
270
+ grants = facultyOf(harbor, GRANTS) ?? null;
271
+ if (!grants)
272
+ throw new Error('this box has no grants');
273
+ grants.persist = () => harbor.wards.get(DOCK)?.save() ?? Promise.resolve();
274
+ const resolve = async (identity, ward) => {
275
+ const hosted = harbor.wards.get(ward);
276
+ return hosted ? find(hosted, identity) : { error: 'no such world' };
277
+ };
278
+ const join = async (identity, invitation, ward) => {
279
+ const hosted = harbor.wards.get(ward);
280
+ if (!hosted)
281
+ return { error: 'no such world' };
282
+ const r = await letIn(hosted, identity, invitation);
283
+ return r.error ? { error: r.error } : {};
284
+ };
285
+ const routes = options.routes ?? (await readRoutes(harbor.dir));
286
+ const here = `http://${http.host}:${http.port}`;
287
+ const at = { quo: routes?.quo ?? `${here}/quo`, web: routes?.web ?? `${here}/web` };
288
+ // a stranger's public being as {}, or an occupant's avatar under a
289
+ // bearer; `web` is for its one human arrival, a redirect landing back
290
+ // which ward a caller who names none is at is the terrain's word, and
291
+ // on a disk it is the mark an operator wrote, read below at `defaultWard`
292
+ http.mount('/api', apiRoute({ wards: harbor.wards, here: () => harbor.defaultWard }, { grants, resolve, join, web: at.web }));
293
+ // The web ring, under the world's own doorbell key: the push faculty
294
+ // mints it the first time this box serves a web route and keeps it in
295
+ // her cells, so it lives with the wards and in no file. The ring is
296
+ // Web Push signed under it, and the subject a push service may
297
+ // complain to is this world's own web route, which every world has.
298
+ const push = facultyOf(harbor, PUSH);
299
+ if (!push)
300
+ throw new Error('this box has no push faculty');
301
+ const key = await push.mint();
302
+ rings.web ??= webPush({ key, subject: new URL(at.web).origin });
303
+ // The two rings the stores own. A world holds no platform key and
304
+ // never will, because Apple and Google sign a push only for the team
305
+ // that published the app, so a ring to a phone is handed to a bell
306
+ // service, a token and nothing else: the one the token names, which
307
+ // is the service of whichever app the phone runs, or the one the root
308
+ // kept on the push faculty for a token naming none. The dock names no
309
+ // address of its own, since whose service it is, is the estate's
310
+ // business and not this package's. A world told of none rings only
311
+ // the phones whose tokens say where.
312
+ //
313
+ // A box holding Apple's own key is the one exception, and it is not a
314
+ // world: it is a bell, whose wards are doorbells and whose whole job
315
+ // is to hold that key. Where it stands is a Node box, whose `fetch`
316
+ // speaks HTTP/1.1 and which Apple refuses, so its ring is the one
317
+ // over `node:http2`. A box handed no key routes, which is every box
318
+ // but a bell's.
319
+ const apple = apnsOf(process.env);
320
+ rings.apns ??= apple ? apnsHttp2(apple) : { ring: (t) => routed(push.service).ring(t) };
321
+ rings.fcm ??= { ring: (t) => routed(push.service).ring(t) };
322
+ // the worlds' pages: always, on the daemon's own door when no route names a public one
323
+ // the bundles the dock's build emitted: read here, because reading a
324
+ // file is the terrain's and a route is handed the code
325
+ 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) });
326
+ if (routes) {
327
+ // the worlds a client may be allowed into: every ward here but the
328
+ // dock, which is the box's and nobody's world, by name and pk, the device's own first
329
+ const worlds = () => [...harbor.wards]
330
+ .filter(([ward]) => ward !== DOCK)
331
+ .sort(([a], [b]) => (a === harbor.defaultWard ? -1 : b === harbor.defaultWard ? 1 : a.localeCompare(b)))
332
+ .map(([ward, h]) => ({ ward, pk: h.pk, user: h.record.user }));
333
+ // the MCP endpoint: a bearer names an identity in a world, the identity names her avatar there, the side runs beside her
334
+ mcp = new McpHttp(resolve, async () => {
335
+ for (const h of harbor.wards.values())
336
+ await h.save();
337
+ });
338
+ const route = await mcpRoute(danceOnDisk(harbor.dir), routes, mcp, grants);
339
+ oauth = route.oauth;
340
+ http.mount('/mcp', route.handler);
341
+ 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
342
+ const g = grants;
343
+ mcp.gone = (identity, ward) => g.revoke(identity, ward); // removal at the ward ends the grant at the box
344
+ exchange = new Exchange({ oauth, worlds, join });
345
+ }
346
+ const ex = exchange;
347
+ http.mount('/web', async (req, rest) =>
348
+ // the worlds' pages first, then the exchange pages that share the
349
+ // prefix; both answer a `Response` and decline by answering
350
+ // nothing, and a path neither takes is this mount's own 404
351
+ (await web(req, rest)) ??
352
+ (ex && (await ex.handle(req, rest))) ??
353
+ new Response('no such page', { status: 404, headers: { 'content-type': 'text/plain' } }));
354
+ }
355
+ // the routes this box holds a socket to are rows on the dialers faculty,
356
+ // dialed from her cells at every boot; `dial` is a suite's way of placing one
357
+ for (const url of options.dial ?? []) {
358
+ const kept = dialers.keep({ url });
359
+ if ('error' in kept)
360
+ throw new Error(String(kept.error));
361
+ }
362
+ dialers.start(harbor);
363
+ // Which of its wards is the default: the agents and the ward the door
364
+ // answers for hang off it, and a guess failing in silence is not a way
365
+ // to find out.
366
+ harbor.defaultWard = defaultWard(await DiskHarbor.marked(harbor.dir), [...harbor.wards.keys()]);
367
+ // The agents faculty holds the table; this daemon is what runs a row:
368
+ // the identity admitted into her ward on the local proof, and the agent
369
+ // side over her avatar for as long as the daemon serves.
370
+ agents = facultyOf(harbor, AGENTS) ?? null;
371
+ if (!agents)
372
+ throw new Error('this box has no agents faculty');
373
+ await agents.start(async (row) => {
374
+ const hosted = harbor.wards.get(row.ward);
375
+ if (!hosted)
376
+ throw new Error(`agent ${row.id}: this device hosts no ward ${row.ward}`);
377
+ const admitted = await admit(harbor, hosted, row.id);
378
+ if (!admitted.avatar)
379
+ throw new Error(`agent ${row.id}: ${admitted.error}`);
380
+ // a process is the agent side, a model is the runner: both take a push as the event
381
+ return isProcess(row) ? agentSide(admitted.avatar, processRun(row.command, row.args, row.dir), () => hosted.save()) : runnerSide(admitted.avatar, row, () => { }, () => hosted.save());
382
+ });
383
+ }
384
+ catch (e) {
385
+ for (const s of servers)
386
+ s.close();
387
+ await agents?.close();
388
+ dialers.close();
389
+ await harbor.close(); // no daemon, no lease
390
+ throw e;
391
+ }
392
+ return {
393
+ harbor,
394
+ sock,
395
+ side,
396
+ http,
397
+ grants,
398
+ oauth,
399
+ exchange,
400
+ mcp,
401
+ agents,
402
+ quo,
403
+ // The tick the timer makes, at a moment somebody names: a suite proves a
404
+ // wake without waiting a minute for one, and an operator can make the
405
+ // daemon fire what is due right now.
406
+ tick: (now) => tick(harbor, now),
407
+ close: async () => {
408
+ facultyOf(harbor, TIMER)?.stop();
409
+ await mcp?.close();
410
+ await agents?.close();
411
+ dialers.close();
412
+ for (const s of quo.sockets)
413
+ s.close();
414
+ for (const s of servers)
415
+ await new Promise((ok) => s.close(() => ok()));
416
+ await unlink(sock).catch(() => { });
417
+ await unlink(side).catch(() => { });
418
+ await harbor.close();
419
+ },
420
+ };
421
+ }
422
+ // A side connection: hello, then MCP over the same lines, or, with `run`,
423
+ // the human's lines in and the model's final texts out.
424
+ async function sideConnection(harbor, c) {
425
+ c.on('error', () => { });
426
+ const lines = createInterface({ input: c });
427
+ const hello = await new Promise((ok) => lines.once('line', ok));
428
+ let as, hosted, run;
429
+ try {
430
+ const req = JSON.parse(hello);
431
+ hosted = harbor.wards.get(req.ward ?? harbor.defaultWard);
432
+ if (!hosted)
433
+ throw new Error('no such ward');
434
+ if (typeof req.as !== 'string' || !/^[\w.-]+$/.test(req.as) || req.as === hosted.record.user || req.as === 'desk')
435
+ throw new Error('as is a word, and not a being of the ward');
436
+ as = req.as;
437
+ if (req.run !== undefined) {
438
+ const r = req.run;
439
+ if (typeof r.url !== 'string' || typeof r.model !== 'string')
440
+ throw new Error('run names a model: { url, model }');
441
+ run = { url: r.url, model: r.model, ...(typeof r.key === 'string' ? { key: r.key } : {}), ...(typeof r.turns === 'number' ? { turns: r.turns } : {}) };
442
+ }
443
+ }
444
+ catch (e) {
445
+ c.end(JSON.stringify({ error: e instanceof Error ? e.message : String(e) }) + '\n');
446
+ return;
447
+ }
448
+ const admitted = await admit(harbor, hosted, as);
449
+ if (!admitted.avatar)
450
+ return void c.end(JSON.stringify({ error: admitted.error }) + '\n');
451
+ const avatar = admitted.avatar;
452
+ c.write(JSON.stringify({ ok: as }) + '\n');
453
+ if (run) {
454
+ const runner = runnerSide(avatar, run, (text) => c.write(text + '\n'), () => hosted.save());
455
+ lines.on('line', (line) => {
456
+ runner.say(line).then((text) => {
457
+ if (text === null)
458
+ c.write('\n'); // the ceiling: a turn with no final text is an empty line
459
+ }, (e) => c.write(JSON.stringify({ error: e instanceof Error ? e.message : String(e) }) + '\n'));
460
+ });
461
+ c.once('close', () => void runner.close().catch(() => { }));
462
+ return;
463
+ }
464
+ const transport = {
465
+ start: async () => { },
466
+ send: async (m) => {
467
+ c.write(JSON.stringify(m) + '\n');
468
+ },
469
+ close: async () => {
470
+ c.end();
471
+ },
472
+ };
473
+ lines.on('line', (line) => {
474
+ try {
475
+ transport.onmessage?.(JSON.parse(line));
476
+ }
477
+ catch (e) {
478
+ transport.onerror?.(e instanceof Error ? e : new Error(String(e)));
479
+ }
480
+ });
481
+ const serving = await mcpSide(avatar, transport, () => hosted.save());
482
+ c.once('close', () => {
483
+ transport.onclose?.();
484
+ void serving.close().catch(() => { });
485
+ });
486
+ }
487
+ async function handle(harbor, line) {
488
+ let id = null;
489
+ try {
490
+ const req = JSON.parse(line);
491
+ id = req.id ?? null;
492
+ if (req.reach) {
493
+ // the directory is the harbor's, not a ward's: a hint from a link
494
+ if (typeof req.reach.pk !== 'string' || !/^[0-9a-f]{128}$/.test(req.reach.pk) || typeof req.reach.url !== 'string')
495
+ return JSON.stringify({ id, error: 'a reach is a ward pk and a URL' });
496
+ await harbor.remember(req.reach.pk, req.reach.url);
497
+ return JSON.stringify({ id, result: { reach: req.reach.pk, url: req.reach.url } });
498
+ }
499
+ const hosted = harbor.wards.get(req.ward ?? harbor.defaultWard);
500
+ if (!hosted)
501
+ return JSON.stringify({ id, error: 'no such ward' });
502
+ // Reaching into a being is the owner's seventh ask, so the socket forwards
503
+ // it like the other six and holds nothing of its own: the ward names the
504
+ // asker, bounds the wait, judges her answer and says it wrote, and the
505
+ // pointer saves before it returns.
506
+ if (req.being !== undefined) {
507
+ if (typeof req.being !== 'string')
508
+ return JSON.stringify({ id, error: 'a being is named by its key' });
509
+ const out = await hosted.ask('ask', { being: req.being, method: req.method, args: req.args ?? {} });
510
+ return JSON.stringify({ id, result: isSilence(out) ? { silence: true } : out });
511
+ }
512
+ if (req.via !== undefined) {
513
+ // owning another ward is one of the human's standings, so her user
514
+ // being holds it, and the root asks on it. The keys rotate in the
515
+ // partition with every ask, and no door was passed, so the harbor
516
+ // saves here.
517
+ if (typeof req.via !== 'string')
518
+ return JSON.stringify({ id, error: 'via is a standing of the user being' });
519
+ const user = hosted.being(hosted.record.user);
520
+ const st = user?.standings[req.via];
521
+ if (!st)
522
+ return JSON.stringify({ id, error: `no standing ${req.via} at the user being` });
523
+ try {
524
+ const out = await st.ask(req.method, (req.args ?? {}));
525
+ return JSON.stringify({ id, result: isSilence(out) ? { silence: true } : isWord(out) ? { word: wordOf(out) } : out });
526
+ }
527
+ finally {
528
+ await hosted.save();
529
+ }
530
+ }
531
+ const out = await hosted.ask(req.method, req.args ?? {});
532
+ return JSON.stringify({ id, result: isSilence(out) ? { silence: true } : out });
533
+ }
534
+ catch (e) {
535
+ return JSON.stringify({ id, error: e instanceof Error ? e.message : String(e) });
536
+ }
537
+ }
538
+ // The four routes by the one convention: `mcp.`, `web.`, `quo.` and `api.`
539
+ // under one domain, over https. A domain is said once and the names follow;
540
+ // an estate whose names break the convention edits the file it wrote.
541
+ export const DOMAIN = /^[a-z0-9.-]+\.[a-z]{2,}$/i;
542
+ export function routesFor(domain) {
543
+ if (!DOMAIN.test(domain))
544
+ throw new Error(`${domain} is not a domain`);
545
+ return { mcp: `https://mcp.${domain}`, web: `https://web.${domain}`, quo: `https://quo.${domain}`, api: `https://api.${domain}` };
546
+ }
547
+ export async function writeRoutes(dir, domain) {
548
+ const routes = routesFor(domain);
549
+ await mkdir(dir, { recursive: true });
550
+ await writeFile(join(dir, 'routes.json'), JSON.stringify(routes) + '\n');
551
+ return routes;
552
+ }
553
+ // `<dir>/routes.json`: { mcp, web, quo?, api? }, the public origins the proxy serves.
554
+ export async function readRoutes(dir) {
555
+ const p = join(dir, 'routes.json');
556
+ if (!existsSync(p))
557
+ return null;
558
+ const r = JSON.parse(await readFile(p, 'utf8'));
559
+ if (typeof r.mcp !== 'string' || typeof r.web !== 'string')
560
+ return null;
561
+ const out = { mcp: r.mcp.replace(/\/$/, ''), web: r.web.replace(/\/$/, '') };
562
+ if (typeof r.quo === 'string')
563
+ out.quo = r.quo.replace(/\/$/, '');
564
+ if (typeof r.api === 'string')
565
+ out.api = r.api.replace(/\/$/, '');
566
+ return out;
567
+ }
568
+ // `<dir>/app.json`: { apple: [...], android: [{ package, fingerprints }] }, the
569
+ // apps whose links this world's hostname opens, as the two platforms want
570
+ // them named. An estate that ships no app of its own writes no such file,
571
+ // serves no association file, and its links open the tab.
572
+ export async function readApps(dir) {
573
+ const p = join(dir, 'app.json');
574
+ if (!existsSync(p))
575
+ return null;
576
+ const r = JSON.parse(await readFile(p, 'utf8'));
577
+ const apple = Array.isArray(r.apple) ? r.apple.filter((x) => typeof x === 'string') : [];
578
+ const android = Array.isArray(r.android)
579
+ ? r.android
580
+ .filter((a) => typeof a?.package === 'string' && Array.isArray(a.fingerprints))
581
+ .map((a) => ({ package: a.package, fingerprints: a.fingerprints.filter((f) => typeof f === 'string') }))
582
+ : [];
583
+ return apple.length === 0 && android.length === 0 ? null : { apple, android };
584
+ }
@@ -0,0 +1,33 @@
1
+ # Four hostnames, one daemon, one loopback port. Caddy faces the world and
2
+ # terminates TLS; the daemon never does. Each hostname is a route, and a
3
+ # route is a path on the daemon's HTTP door. A route says the grammar a
4
+ # caller speaks and never who the caller is: a bearer the box granted is
5
+ # honoured on every one of them. Replace DOMAIN.
6
+ #
7
+ # mcp. the model side: streamable HTTP MCP, with the OAuth dance in front
8
+ # web. the bundle for plain tabs, and the exchange pages
9
+ # quo. the socket door for other harbors: requests in, sockets held, the rendezvous
10
+ # api. a describe and its asks as plain JSON, for a program
11
+ #
12
+ # There is no cli. route. The root owner is the unix socket, reached on the
13
+ # device or over SSH, and never through here.
14
+
15
+ mcp.DOMAIN {
16
+ reverse_proxy 127.0.0.1:8787
17
+ rewrite * /mcp{uri}
18
+ }
19
+
20
+ web.DOMAIN {
21
+ reverse_proxy 127.0.0.1:8787
22
+ rewrite * /web{uri}
23
+ }
24
+
25
+ quo.DOMAIN {
26
+ reverse_proxy 127.0.0.1:8787
27
+ rewrite * /quo{uri}
28
+ }
29
+
30
+ api.DOMAIN {
31
+ reverse_proxy 127.0.0.1:8787
32
+ rewrite * /api{uri}
33
+ }