@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.
- package/GETTING_STARTED.md +128 -0
- package/LICENSE +202 -0
- package/NOTICE +6 -0
- package/README.md +37 -0
- package/api/quo-api.md +196 -0
- package/api/route.ts +227 -0
- package/beings/GLOSSARY.md +118 -0
- package/beings/WORLDS.md +567 -0
- package/beings/avatar.ts +101 -0
- package/beings/carry.ts +159 -0
- package/beings/clock.ts +552 -0
- package/beings/courier.ts +107 -0
- package/beings/desk.ts +128 -0
- package/beings/doorbell.ts +129 -0
- package/beings/envoy.ts +218 -0
- package/beings/index.ts +16 -0
- package/beings/join.ts +51 -0
- package/beings/link.ts +104 -0
- package/beings/look.ts +139 -0
- package/beings/porter.ts +43 -0
- package/beings/presence.ts +39 -0
- package/beings/quo-app-bells.md +280 -0
- package/beings/quo-dock.md +1942 -0
- package/beings/setup.ts +45 -0
- package/beings/side.ts +60 -0
- package/beings/user.ts +459 -0
- package/beings/way.ts +65 -0
- package/cli/bundles.ts +20 -0
- package/cli/client.ts +73 -0
- package/cli/daemon.ts +581 -0
- package/cli/estate/Caddyfile +33 -0
- package/cli/estate/quo.service +35 -0
- package/cli/estate/systems.quo.plist +44 -0
- package/cli/estate.ts +48 -0
- package/cli/folder.ts +27 -0
- package/cli/oauth.ts +32 -0
- package/cli/pilot.ts +38 -0
- package/cli/quo-dock.md +1942 -0
- package/cli/quo.ts +203 -0
- package/cli/stand.ts +160 -0
- package/dist/api/route.d.ts +21 -0
- package/dist/api/route.js +193 -0
- package/dist/beings/avatar.d.ts +32 -0
- package/dist/beings/avatar.js +100 -0
- package/dist/beings/carry.d.ts +12 -0
- package/dist/beings/carry.js +174 -0
- package/dist/beings/clock.d.ts +161 -0
- package/dist/beings/clock.js +528 -0
- package/dist/beings/courier.d.ts +50 -0
- package/dist/beings/courier.js +96 -0
- package/dist/beings/desk.d.ts +53 -0
- package/dist/beings/desk.js +135 -0
- package/dist/beings/doorbell.d.ts +78 -0
- package/dist/beings/doorbell.js +133 -0
- package/dist/beings/envoy.d.ts +54 -0
- package/dist/beings/envoy.js +211 -0
- package/dist/beings/index.d.ts +14 -0
- package/dist/beings/index.js +16 -0
- package/dist/beings/join.d.ts +15 -0
- package/dist/beings/join.js +42 -0
- package/dist/beings/link.d.ts +14 -0
- package/dist/beings/link.js +91 -0
- package/dist/beings/look.d.ts +30 -0
- package/dist/beings/look.js +104 -0
- package/dist/beings/porter.d.ts +14 -0
- package/dist/beings/porter.js +41 -0
- package/dist/beings/presence.d.ts +8 -0
- package/dist/beings/presence.js +21 -0
- package/dist/beings/setup.d.ts +3 -0
- package/dist/beings/setup.js +39 -0
- package/dist/beings/side.d.ts +32 -0
- package/dist/beings/side.js +37 -0
- package/dist/beings/user.d.ts +315 -0
- package/dist/beings/user.js +462 -0
- package/dist/beings/way.d.ts +19 -0
- package/dist/beings/way.js +28 -0
- package/dist/cli/bundles.d.ts +5 -0
- package/dist/cli/bundles.js +17 -0
- package/dist/cli/client.d.ts +15 -0
- package/dist/cli/client.js +58 -0
- package/dist/cli/daemon.d.ts +50 -0
- package/dist/cli/daemon.js +584 -0
- package/dist/cli/estate/Caddyfile +33 -0
- package/dist/cli/estate/quo.service +35 -0
- package/dist/cli/estate/systems.quo.plist +44 -0
- package/dist/cli/estate.d.ts +5 -0
- package/dist/cli/estate.js +50 -0
- package/dist/cli/folder.d.ts +2 -0
- package/dist/cli/folder.js +25 -0
- package/dist/cli/oauth.d.ts +2 -0
- package/dist/cli/oauth.js +32 -0
- package/dist/cli/pilot.d.ts +5 -0
- package/dist/cli/pilot.js +22 -0
- package/dist/cli/quo.d.ts +2 -0
- package/dist/cli/quo.js +201 -0
- package/dist/cli/stand.d.ts +37 -0
- package/dist/cli/stand.js +110 -0
- package/dist/dock/agents.d.ts +102 -0
- package/dist/dock/agents.js +105 -0
- package/dist/dock/apns.d.ts +27 -0
- package/dist/dock/apns.js +98 -0
- package/dist/dock/console.d.ts +6 -0
- package/dist/dock/console.js +20 -0
- package/dist/dock/dialers.d.ts +56 -0
- package/dist/dock/dialers.js +75 -0
- package/dist/dock/faculty.d.ts +24 -0
- package/dist/dock/faculty.js +142 -0
- package/dist/dock/fcm.d.ts +13 -0
- package/dist/dock/fcm.js +77 -0
- package/dist/dock/grants.d.ts +41 -0
- package/dist/dock/grants.js +99 -0
- package/dist/dock/http2.d.ts +4 -0
- package/dist/dock/http2.js +68 -0
- package/dist/dock/index.d.ts +41 -0
- package/dist/dock/index.js +113 -0
- package/dist/dock/local.d.ts +13 -0
- package/dist/dock/local.js +44 -0
- package/dist/dock/location.d.ts +19 -0
- package/dist/dock/location.js +11 -0
- package/dist/dock/notifications.d.ts +38 -0
- package/dist/dock/notifications.js +60 -0
- package/dist/dock/push.d.ts +79 -0
- package/dist/dock/push.js +132 -0
- package/dist/dock/service.d.ts +4 -0
- package/dist/dock/service.js +107 -0
- package/dist/dock/timer.d.ts +52 -0
- package/dist/dock/timer.js +149 -0
- package/dist/dock/webpush.d.ts +12 -0
- package/dist/dock/webpush.js +144 -0
- package/dist/harbor/browser.d.ts +15 -0
- package/dist/harbor/browser.js +82 -0
- package/dist/harbor/capacitor.d.ts +28 -0
- package/dist/harbor/capacitor.js +218 -0
- package/dist/harbor/disk.d.ts +27 -0
- package/dist/harbor/disk.js +249 -0
- package/dist/harbor/edge/alarm.d.ts +10 -0
- package/dist/harbor/edge/alarm.js +38 -0
- package/dist/harbor/edge/edge.d.ts +54 -0
- package/dist/harbor/edge/edge.js +370 -0
- package/dist/harbor/edge/exercise.d.ts +7 -0
- package/dist/harbor/edge/exercise.js +232 -0
- package/dist/harbor/edge/given.d.ts +26 -0
- package/dist/harbor/edge/given.js +42 -0
- package/dist/harbor/edge/platform.d.ts +39 -0
- package/dist/harbor/edge/storage.d.ts +30 -0
- package/dist/harbor/edge/storage.js +180 -0
- package/dist/harbor/edge/worker.d.ts +17 -0
- package/dist/harbor/edge/worker.js +67 -0
- package/dist/harbor/files.d.ts +15 -0
- package/dist/harbor/files.js +220 -0
- package/dist/harbor/http.d.ts +19 -0
- package/dist/harbor/http.js +177 -0
- package/dist/harbor/idb.d.ts +15 -0
- package/dist/harbor/idb.js +0 -0
- package/dist/harbor/quo.d.ts +10 -0
- package/dist/harbor/quo.js +42 -0
- package/dist/harbor/resolve.d.ts +16 -0
- package/dist/harbor/resolve.js +17 -0
- package/dist/harbor/seal.d.ts +3 -0
- package/dist/harbor/seal.js +25 -0
- package/dist/harbor/sealed.d.ts +35 -0
- package/dist/harbor/sealed.js +162 -0
- package/dist/harbor/tauri.d.ts +16 -0
- package/dist/harbor/tauri.js +59 -0
- package/dist/harbor/ward.d.ts +9 -0
- package/dist/harbor/ward.js +15 -0
- package/dist/human/app.d.ts +32 -0
- package/dist/human/app.js +527 -0
- package/dist/human/apps.d.ts +10 -0
- package/dist/human/apps.js +29 -0
- package/dist/human/dom.d.ts +2 -0
- package/dist/human/dom.js +15 -0
- package/dist/human/door.d.ts +19 -0
- package/dist/human/door.js +92 -0
- package/dist/human/guest.d.ts +3 -0
- package/dist/human/guest.js +33 -0
- package/dist/human/html.d.ts +55 -0
- package/dist/human/html.js +451 -0
- package/dist/human/local.d.ts +7 -0
- package/dist/human/local.js +10 -0
- package/dist/human/mark.d.ts +8 -0
- package/dist/human/mark.js +47 -0
- package/dist/human/move.d.ts +17 -0
- package/dist/human/move.js +90 -0
- package/dist/human/screen.d.ts +19 -0
- package/dist/human/screen.js +119 -0
- package/dist/human/style.d.ts +2 -0
- package/dist/human/style.js +60 -0
- package/dist/human/tab.bundle.txt +4775 -0
- package/dist/human/tab.d.ts +15 -0
- package/dist/human/tab.js +409 -0
- package/dist/human/tree.d.ts +50 -0
- package/dist/human/tree.js +157 -0
- package/dist/human/wake.d.ts +34 -0
- package/dist/human/wake.js +179 -0
- package/dist/human/web.d.ts +33 -0
- package/dist/human/web.js +184 -0
- package/dist/human/worker.bundle.txt +3868 -0
- package/dist/human/worker.d.ts +1 -0
- package/dist/human/worker.js +81 -0
- package/dist/human/worlds.d.ts +24 -0
- package/dist/human/worlds.js +99 -0
- package/dist/mcp/agent.d.ts +10 -0
- package/dist/mcp/agent.js +78 -0
- package/dist/mcp/allow.d.ts +20 -0
- package/dist/mcp/allow.js +88 -0
- package/dist/mcp/http.d.ts +27 -0
- package/dist/mcp/http.js +92 -0
- package/dist/mcp/oauth.d.ts +91 -0
- package/dist/mcp/oauth.js +241 -0
- package/dist/mcp/route.d.ts +15 -0
- package/dist/mcp/route.js +42 -0
- package/dist/mcp/runner.d.ts +45 -0
- package/dist/mcp/runner.js +99 -0
- package/dist/mcp/server.d.ts +11 -0
- package/dist/mcp/server.js +102 -0
- package/dock/agents.ts +121 -0
- package/dock/apns.ts +138 -0
- package/dock/console.ts +23 -0
- package/dock/dialers.ts +85 -0
- package/dock/faculty.ts +148 -0
- package/dock/fcm.ts +103 -0
- package/dock/grants.ts +111 -0
- package/dock/http2.ts +70 -0
- package/dock/index.ts +133 -0
- package/dock/local.ts +48 -0
- package/dock/location.ts +22 -0
- package/dock/notifications.ts +67 -0
- package/dock/push.ts +161 -0
- package/dock/quo-app-bells.md +280 -0
- package/dock/quo-dock.md +1942 -0
- package/dock/service.ts +100 -0
- package/dock/timer.ts +174 -0
- package/dock/webpush.ts +182 -0
- package/harbor/browser.ts +97 -0
- package/harbor/capacitor.ts +223 -0
- package/harbor/disk.ts +237 -0
- package/harbor/edge/alarm.ts +48 -0
- package/harbor/edge/edge.ts +407 -0
- package/harbor/edge/exercise.ts +241 -0
- package/harbor/edge/given.ts +106 -0
- package/harbor/edge/platform.d.ts +39 -0
- package/harbor/edge/storage.ts +179 -0
- package/harbor/edge/text.d.ts +15 -0
- package/harbor/edge/worker.ts +70 -0
- package/harbor/edge/wrangler.toml +30 -0
- package/harbor/files.ts +221 -0
- package/harbor/http.ts +188 -0
- package/harbor/idb.ts +0 -0
- package/harbor/quo-harbor.md +850 -0
- package/harbor/quo.ts +44 -0
- package/harbor/resolve.ts +31 -0
- package/harbor/seal.ts +26 -0
- package/harbor/sealed.ts +183 -0
- package/harbor/tauri.ts +79 -0
- package/harbor/ward.ts +28 -0
- package/human/app.ts +580 -0
- package/human/apps.ts +47 -0
- package/human/dom.ts +21 -0
- package/human/door.ts +113 -0
- package/human/guest.ts +34 -0
- package/human/html.ts +490 -0
- package/human/local.ts +30 -0
- package/human/mark.ts +53 -0
- package/human/move.ts +122 -0
- package/human/quo-human.md +1042 -0
- package/human/screen.ts +136 -0
- package/human/style.ts +63 -0
- package/human/tab.bundle.txt +4775 -0
- package/human/tab.ts +446 -0
- package/human/tree.ts +217 -0
- package/human/wake.ts +191 -0
- package/human/web.ts +259 -0
- package/human/worker.bundle.txt +3868 -0
- package/human/worker.ts +124 -0
- package/human/worlds.ts +127 -0
- package/mcp/agent.ts +94 -0
- package/mcp/allow.ts +131 -0
- package/mcp/http.ts +92 -0
- package/mcp/oauth.ts +270 -0
- package/mcp/quo-mcp.md +343 -0
- package/mcp/route.ts +45 -0
- package/mcp/runner.ts +136 -0
- package/mcp/server.ts +101 -0
- 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
|
+
}
|