@quo-systems/dock 0.1.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/LICENSE +202 -0
- package/NOTICE +6 -0
- package/README.md +27 -0
- package/beings/avatar.ts +84 -0
- package/beings/desk.ts +46 -0
- package/beings/index.ts +5 -0
- package/beings/quo-dock.md +698 -0
- package/beings/setup.ts +23 -0
- package/beings/side.ts +51 -0
- package/beings/user.ts +78 -0
- package/cli/client.ts +65 -0
- package/cli/daemon.ts +582 -0
- package/cli/quo.ts +126 -0
- package/dist/beings/avatar.d.ts +32 -0
- package/dist/beings/avatar.js +82 -0
- package/dist/beings/desk.d.ts +42 -0
- package/dist/beings/desk.js +43 -0
- package/dist/beings/index.d.ts +3 -0
- package/dist/beings/index.js +5 -0
- package/dist/beings/setup.d.ts +2 -0
- package/dist/beings/setup.js +16 -0
- package/dist/beings/side.d.ts +25 -0
- package/dist/beings/side.js +37 -0
- package/dist/beings/user.d.ts +108 -0
- package/dist/beings/user.js +74 -0
- package/dist/cli/client.d.ts +14 -0
- package/dist/cli/client.js +51 -0
- package/dist/cli/daemon.d.ts +57 -0
- package/dist/cli/daemon.js +581 -0
- package/dist/cli/quo.d.ts +2 -0
- package/dist/cli/quo.js +121 -0
- package/dist/harbor/browser.d.ts +13 -0
- package/dist/harbor/browser.js +50 -0
- package/dist/harbor/disk.d.ts +19 -0
- package/dist/harbor/disk.js +126 -0
- package/dist/harbor/edge/edge.d.ts +26 -0
- package/dist/harbor/edge/edge.js +123 -0
- package/dist/harbor/edge/exercise.d.ts +7 -0
- package/dist/harbor/edge/exercise.js +179 -0
- package/dist/harbor/edge/storage.d.ts +20 -0
- package/dist/harbor/edge/storage.js +81 -0
- package/dist/harbor/edge/worker.d.ts +14 -0
- package/dist/harbor/edge/worker.js +47 -0
- package/dist/harbor/files.d.ts +14 -0
- package/dist/harbor/files.js +83 -0
- package/dist/harbor/idb.d.ts +15 -0
- package/dist/harbor/idb.js +85 -0
- package/dist/human/dom.d.ts +2 -0
- package/dist/human/dom.js +15 -0
- package/dist/human/html.d.ts +32 -0
- package/dist/human/html.js +136 -0
- package/dist/human/screen.d.ts +11 -0
- package/dist/human/screen.js +66 -0
- package/dist/human/tab.d.ts +7 -0
- package/dist/human/tab.js +68 -0
- package/dist/mcp/agent.d.ts +10 -0
- package/dist/mcp/agent.js +78 -0
- package/dist/mcp/http.d.ts +27 -0
- package/dist/mcp/http.js +96 -0
- package/dist/mcp/oauth.d.ts +98 -0
- package/dist/mcp/oauth.js +241 -0
- package/dist/mcp/pilot.d.ts +6 -0
- package/dist/mcp/pilot.js +68 -0
- package/dist/mcp/runner.d.ts +45 -0
- package/dist/mcp/runner.js +94 -0
- package/dist/mcp/server.d.ts +9 -0
- package/dist/mcp/server.js +63 -0
- package/dist/mcp/web/exchange.d.ts +23 -0
- package/dist/mcp/web/exchange.js +136 -0
- package/harbor/browser.ts +63 -0
- package/harbor/disk.ts +120 -0
- package/harbor/edge/edge.ts +147 -0
- package/harbor/edge/exercise.ts +187 -0
- package/harbor/edge/platform.d.ts +28 -0
- package/harbor/edge/storage.ts +82 -0
- package/harbor/edge/worker.ts +47 -0
- package/harbor/edge/wrangler.toml +21 -0
- package/harbor/files.ts +89 -0
- package/harbor/idb.ts +90 -0
- package/harbor/quo-harbor.md +403 -0
- package/human/dom.ts +21 -0
- package/human/html.ts +164 -0
- package/human/quo-human.md +86 -0
- package/human/screen.ts +76 -0
- package/human/tab.ts +84 -0
- package/mcp/agent.ts +94 -0
- package/mcp/http.ts +96 -0
- package/mcp/oauth.ts +260 -0
- package/mcp/pilot.ts +73 -0
- package/mcp/quo-mcp.md +262 -0
- package/mcp/runner.ts +131 -0
- package/mcp/server.ts +67 -0
- package/mcp/web/exchange.ts +146 -0
- package/package.json +89 -0
package/cli/quo.ts
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
// `quo`: the one command of the estate. Thin on purpose: each subcommand
|
|
4
|
+
// picks a folder, reaches the daemon or the disk, and hands off. Logic that
|
|
5
|
+
// appears here was missing from a being or a side, and moves there.
|
|
6
|
+
//
|
|
7
|
+
// quo init [--dir D] [--ward W] mint a seed, boot a ward over a disk store, write both
|
|
8
|
+
// quo serve [--dir D] [--http PORT] [--dial URL] the daemon: harbor from disk, the two sockets, the HTTP door on loopback, a socket held to URL
|
|
9
|
+
// quo census [--dir D] [--ward W] [--via S] the empty ask: pk and every being
|
|
10
|
+
// quo boot [--dir D] [--ward W] [--via S] <json> { key, class, public? }
|
|
11
|
+
// quo invite [--dir D] [--ward W] [--via S] <json> { being, id }
|
|
12
|
+
// quo knock [--dir D] [--ward W] [--via S] <json> { being | { boot, key }, id, invitation, method?, args?, wanted? }
|
|
13
|
+
// quo remove [--dir D] [--ward W] [--via S] <json> { being, id }: a relation out of a being; on the ward pk, an owner
|
|
14
|
+
// quo side [--dir D] [--ward W] --as NAME an avatar over stdio for a local MCP client; NAME is the client identity
|
|
15
|
+
// quo run [--dir D] [--ward W] --as NAME --url URL --model NAME [--turns N] a human at a terminal talking to a model through an avatar: a line in, the model's final text out
|
|
16
|
+
// quo pilot [--dir D] [--ward W] [--via S] the owner pilot over stdio: census, boot, invite, knock, remove
|
|
17
|
+
// quo reach [--dir D] <pk> <url> a hint for the harbor's directory: that ward pk lives at that quo. route
|
|
18
|
+
//
|
|
19
|
+
// D defaults to $QUO_DIR, then ~/.quo. W defaults to main. init takes
|
|
20
|
+
// --user NAME for the user being, defaulting to the device's user. run
|
|
21
|
+
// takes the model's base URL as --url or $QUO_MODEL_URL, its name as
|
|
22
|
+
// --model or $QUO_MODEL, a bearer from $QUO_MODEL_KEY, read here and never
|
|
23
|
+
// kept, and --turns as the ceiling of model calls in one turn. S is a
|
|
24
|
+
// standing the user being holds at another ward, one the root there invited
|
|
25
|
+
// as an owner: with --via, the owner asks go to that ward's door, sealed,
|
|
26
|
+
// and the answers are its. Without it they are the root's, on the pointer.
|
|
27
|
+
import { homedir, userInfo } from 'node:os';
|
|
28
|
+
import { join } from 'node:path';
|
|
29
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
30
|
+
import { DiskHarbor } from '../harbor/disk.ts';
|
|
31
|
+
import { serve } from './daemon.ts';
|
|
32
|
+
import { ask, side, reach, type Run } from './client.ts';
|
|
33
|
+
import { pilotSide } from '../mcp/pilot.ts';
|
|
34
|
+
|
|
35
|
+
const OWNER_ASKS = new Set(['boot', 'invite', 'knock', 'remove']);
|
|
36
|
+
|
|
37
|
+
function parse(argv: string[]) {
|
|
38
|
+
const flags: Record<string, string> = {};
|
|
39
|
+
const rest: string[] = [];
|
|
40
|
+
for (let i = 0; i < argv.length; i++) {
|
|
41
|
+
const a = argv[i] ?? '';
|
|
42
|
+
if (a.startsWith('--')) flags[a.slice(2)] = argv[++i] ?? '';
|
|
43
|
+
else rest.push(a);
|
|
44
|
+
}
|
|
45
|
+
const [cmd, json, more] = rest;
|
|
46
|
+
return { cmd, json, more, flags, dir: flags.dir ?? process.env.QUO_DIR ?? join(homedir(), '.quo'), ward: flags.ward ?? 'main', via: flags.via };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const print = (v: unknown) => console.log(JSON.stringify(v, null, 2));
|
|
50
|
+
|
|
51
|
+
async function main(argv: string[]): Promise<number> {
|
|
52
|
+
const { cmd, json, more, flags, dir, ward, via } = parse(argv);
|
|
53
|
+
if (cmd === 'init') {
|
|
54
|
+
print(await DiskHarbor.init(dir, ward, flags.user ?? userInfo().username));
|
|
55
|
+
return 0;
|
|
56
|
+
}
|
|
57
|
+
if (cmd === 'side' || cmd === 'run') {
|
|
58
|
+
if (!flags.as) {
|
|
59
|
+
console.error(`quo ${cmd} needs --as NAME`);
|
|
60
|
+
return 2;
|
|
61
|
+
}
|
|
62
|
+
let run: Run | undefined;
|
|
63
|
+
if (cmd === 'run') {
|
|
64
|
+
const url = flags.url ?? process.env.QUO_MODEL_URL;
|
|
65
|
+
const model = flags.model ?? process.env.QUO_MODEL;
|
|
66
|
+
if (!url || !model) {
|
|
67
|
+
console.error('quo run needs --url URL and --model NAME, or QUO_MODEL_URL and QUO_MODEL');
|
|
68
|
+
return 2;
|
|
69
|
+
}
|
|
70
|
+
const key = process.env.QUO_MODEL_KEY;
|
|
71
|
+
const turns = flags.turns !== undefined ? Number(flags.turns) : undefined;
|
|
72
|
+
if (turns !== undefined && !(Number.isInteger(turns) && turns > 0)) {
|
|
73
|
+
console.error('--turns is a positive integer');
|
|
74
|
+
return 2;
|
|
75
|
+
}
|
|
76
|
+
run = { url, model, ...(key ? { key } : {}), ...(turns !== undefined ? { turns } : {}) };
|
|
77
|
+
}
|
|
78
|
+
await side(dir, flags.as, ward, run); // stdio to the daemon, until one end closes
|
|
79
|
+
return -1;
|
|
80
|
+
}
|
|
81
|
+
if (cmd === 'pilot') {
|
|
82
|
+
await ask(dir, undefined, {}, ward); // no daemon is a clear word now, not at the first tool call
|
|
83
|
+
const serving = await pilotSide(dir, ward, new StdioServerTransport(), (line) => console.error(`pilot ${line}`), via);
|
|
84
|
+
process.stdin.once('end', () => void serving.close().then(() => process.exit(0)));
|
|
85
|
+
return -1;
|
|
86
|
+
}
|
|
87
|
+
if (cmd === 'serve') {
|
|
88
|
+
const port = flags.http ?? process.env.QUO_HTTP;
|
|
89
|
+
const s = await serve(dir, { ...(port !== undefined ? { http: { port: Number(port) } } : {}), ...(flags.dial ? { dial: [flags.dial] } : {}) });
|
|
90
|
+
print({ dir: s.harbor.dir, sock: s.sock, side: s.side, http: s.http ? `http://${s.http.host}:${s.http.port}` : null, dial: s.quo.dialers.map((d) => d.url), wards: Object.fromEntries([...s.harbor.wards].map(([n, h]) => [n, h.pk])) });
|
|
91
|
+
const stop = () => void s.close().then(() => process.exit(0));
|
|
92
|
+
process.once('SIGINT', stop);
|
|
93
|
+
process.once('SIGTERM', stop);
|
|
94
|
+
return -1; // stays up
|
|
95
|
+
}
|
|
96
|
+
if (cmd === 'reach') {
|
|
97
|
+
if (!json || !more) {
|
|
98
|
+
console.error('quo reach needs <pk> <url>');
|
|
99
|
+
return 2;
|
|
100
|
+
}
|
|
101
|
+
const out = await reach(dir, json, more);
|
|
102
|
+
print('error' in out ? { error: out.error } : out.result);
|
|
103
|
+
return 'error' in out ? 1 : 0;
|
|
104
|
+
}
|
|
105
|
+
if (cmd === 'census' || (cmd !== undefined && OWNER_ASKS.has(cmd))) {
|
|
106
|
+
const args = json ? (JSON.parse(json) as Record<string, unknown>) : {};
|
|
107
|
+
const out = await ask(dir, cmd === 'census' ? undefined : cmd, args, ward, via);
|
|
108
|
+
print('error' in out ? { error: out.error } : out.result);
|
|
109
|
+
// an error object the ward answered is an ordinary answer, and still a
|
|
110
|
+
// non-zero exit, so a script can tell a boot that happened from one that did not.
|
|
111
|
+
const failed = 'error' in out || (typeof out.result === 'object' && out.result !== null && 'error' in out.result);
|
|
112
|
+
return failed ? 1 : 0;
|
|
113
|
+
}
|
|
114
|
+
console.error('usage: quo init|serve|census|boot|invite|knock|remove|side|run|pilot|reach [--dir D] [--ward W] [--via S] [--as NAME] [--url URL --model NAME] [--dial URL] [json]');
|
|
115
|
+
return 2;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
main(process.argv.slice(2)).then(
|
|
119
|
+
(code) => {
|
|
120
|
+
if (code >= 0) process.exit(code);
|
|
121
|
+
},
|
|
122
|
+
(e: unknown) => {
|
|
123
|
+
console.error(e instanceof Error ? e.message : String(e));
|
|
124
|
+
process.exit(1);
|
|
125
|
+
},
|
|
126
|
+
);
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Being } from '@quo-systems/quo';
|
|
2
|
+
import type { Answer, Blueprint, Invitation, JsonObject, OccupantRecord, Wanted } from '@quo-systems/quo';
|
|
3
|
+
export declare const USER = "user";
|
|
4
|
+
export declare const PUSHER = "user:push";
|
|
5
|
+
export declare class Avatar extends Being {
|
|
6
|
+
static cells: {
|
|
7
|
+
inbox: JsonObject[];
|
|
8
|
+
};
|
|
9
|
+
static asks: {
|
|
10
|
+
notify: {
|
|
11
|
+
description: string;
|
|
12
|
+
input: {
|
|
13
|
+
type: string;
|
|
14
|
+
};
|
|
15
|
+
for: (occ: OccupantRecord | undefined) => boolean;
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
readonly ears: Set<(object: JsonObject) => void>;
|
|
19
|
+
notify(args: JsonObject): {
|
|
20
|
+
notified: boolean;
|
|
21
|
+
};
|
|
22
|
+
enter(desk: Invitation, proof: JsonObject): Promise<Blueprint | {
|
|
23
|
+
error: string;
|
|
24
|
+
}>;
|
|
25
|
+
join(invitation: Invitation): Promise<Blueprint | {
|
|
26
|
+
error: string;
|
|
27
|
+
}>;
|
|
28
|
+
tools(): Promise<Blueprint | {
|
|
29
|
+
error: string;
|
|
30
|
+
}>;
|
|
31
|
+
call(name: string, args?: JsonObject, wanted?: Wanted): Promise<Answer>;
|
|
32
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// The avatar. Someone outside Quo's voice: a human at a screen, a model at
|
|
3
|
+
// an MCP client, a model in an API loop. One class for every side. She holds
|
|
4
|
+
// exactly one standing, `user`, and everything her side asks goes through
|
|
5
|
+
// it; the user being reaches her only to push, and she hands the push to
|
|
6
|
+
// every side she has. A side is in-process with her, holds the object, and
|
|
7
|
+
// calls the three methods below. Nothing of a side is in her cells.
|
|
8
|
+
import { Being, isSilence, isWord, wordOf } from '@quo-systems/quo';
|
|
9
|
+
// Her one standing, and the id under which the user being pushes to her.
|
|
10
|
+
// Two ids for one far being, because standings and occupants share one
|
|
11
|
+
// namespace and a record holds one or the other.
|
|
12
|
+
export const USER = 'user';
|
|
13
|
+
export const PUSHER = 'user:push';
|
|
14
|
+
export class Avatar extends Being {
|
|
15
|
+
static cells = { inbox: [] };
|
|
16
|
+
static asks = {
|
|
17
|
+
notify: { description: 'a push from the user being', input: { type: 'object' }, for: (occ) => occ?.id === PUSHER },
|
|
18
|
+
};
|
|
19
|
+
// Her sides' ears. One identity is one avatar for life, and two sessions
|
|
20
|
+
// of that identity are two sides on her at once, so a push goes to every
|
|
21
|
+
// ear present; each side adds its own on open and removes it on close.
|
|
22
|
+
// Dropped if there is none.
|
|
23
|
+
ears = new Set();
|
|
24
|
+
// ---- what the user being may ask her
|
|
25
|
+
notify(args) {
|
|
26
|
+
this.cells.inbox.push(args);
|
|
27
|
+
for (const ear of this.ears)
|
|
28
|
+
ear(args);
|
|
29
|
+
return { notified: true };
|
|
30
|
+
}
|
|
31
|
+
// ---- what her side calls. Not asks: nobody reaches these through a door.
|
|
32
|
+
// The credential exchange's last step. `desk` is the desk's public
|
|
33
|
+
// invitation, `proof` is what the route produced. She knocks the desk as a
|
|
34
|
+
// stranger, gets an invitation the user being minted, and joins with it. A
|
|
35
|
+
// knock on a public invitation births nothing, so she holds no standing at
|
|
36
|
+
// the desk afterwards. If she already holds `user`, this is a reconnect and
|
|
37
|
+
// nothing is minted.
|
|
38
|
+
async enter(desk, proof) {
|
|
39
|
+
if (this.standings[USER])
|
|
40
|
+
return this.tools();
|
|
41
|
+
const inv = await this.knock(desk, 'device', { proof });
|
|
42
|
+
if (isSilence(inv))
|
|
43
|
+
return { error: 'silence' };
|
|
44
|
+
if (isWord(inv))
|
|
45
|
+
return { error: wordOf(inv) };
|
|
46
|
+
if (!inv || typeof inv !== 'object' || Array.isArray(inv) || 'error' in inv)
|
|
47
|
+
return inv ?? { error: 'no invitation' };
|
|
48
|
+
return this.join(inv);
|
|
49
|
+
}
|
|
50
|
+
// Knock with an invitation, hand the user being one back so she can push,
|
|
51
|
+
// and take her as `user`. The only moment a standing is born here.
|
|
52
|
+
async join(invitation) {
|
|
53
|
+
if (this.standings[USER])
|
|
54
|
+
return this.tools();
|
|
55
|
+
const mine = await this.invite(PUSHER);
|
|
56
|
+
if (mine === null)
|
|
57
|
+
return { error: 'user is taken' };
|
|
58
|
+
const out = await this.knock(invitation, 'hello', { invitation: mine });
|
|
59
|
+
if (isSilence(out) || isWord(out)) {
|
|
60
|
+
this.occupants.remove(PUSHER);
|
|
61
|
+
return { error: isSilence(out) ? 'silence' : wordOf(out) };
|
|
62
|
+
}
|
|
63
|
+
await this.take(USER, invitation);
|
|
64
|
+
return this.tools();
|
|
65
|
+
}
|
|
66
|
+
// Her describe, as the user being shows it to her. This is the tool list.
|
|
67
|
+
async tools() {
|
|
68
|
+
const st = this.standings[USER];
|
|
69
|
+
if (!st)
|
|
70
|
+
return { error: 'not joined' };
|
|
71
|
+
const bp = await st.ask();
|
|
72
|
+
if (isSilence(bp) || isWord(bp))
|
|
73
|
+
return { error: isSilence(bp) ? 'silence' : wordOf(bp) };
|
|
74
|
+
return bp;
|
|
75
|
+
}
|
|
76
|
+
// One call from the side, one ask on the standing. The three words pass
|
|
77
|
+
// through untouched; the side renders them.
|
|
78
|
+
call(name, args = {}, wanted) {
|
|
79
|
+
const st = this.standings[USER];
|
|
80
|
+
return st ? st.ask(name, args, wanted) : Promise.resolve({ error: 'not joined' });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Being } from '@quo-systems/quo';
|
|
2
|
+
import type { Asker, Json, JsonObject } from '@quo-systems/quo';
|
|
3
|
+
export type Proof = {
|
|
4
|
+
kind: string;
|
|
5
|
+
[more: string]: Json;
|
|
6
|
+
};
|
|
7
|
+
export type Verified = {
|
|
8
|
+
user: string;
|
|
9
|
+
client: string;
|
|
10
|
+
wake?: boolean;
|
|
11
|
+
};
|
|
12
|
+
export type Verifier = (proof: Proof) => Promise<Verified | null> | Verified | null;
|
|
13
|
+
export declare class Desk extends Being {
|
|
14
|
+
static verifiers: Record<string, Verifier>;
|
|
15
|
+
static asks: {
|
|
16
|
+
hello: {
|
|
17
|
+
description: string;
|
|
18
|
+
input: {
|
|
19
|
+
type: string;
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
device: {
|
|
23
|
+
description: string;
|
|
24
|
+
input: {
|
|
25
|
+
type: string;
|
|
26
|
+
properties: {
|
|
27
|
+
proof: {
|
|
28
|
+
type: string;
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
required: string[];
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
hello(_args: JsonObject, asker: Asker): {
|
|
36
|
+
desk: boolean;
|
|
37
|
+
asker: string | null;
|
|
38
|
+
kinds: string[];
|
|
39
|
+
users: string[];
|
|
40
|
+
};
|
|
41
|
+
device(args: JsonObject): Promise<Json>;
|
|
42
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// The front desk. The one public being of a world, and public for the first
|
|
3
|
+
// hello only: everything after is behind an invitation. It holds a standing
|
|
4
|
+
// to every user being, placed by the root, and trades a proof for a device
|
|
5
|
+
// invitation that the user being mints herself. The desk never mints one.
|
|
6
|
+
//
|
|
7
|
+
// A proof is what a route produced after authenticating by its own means. A
|
|
8
|
+
// verifier turns a proof of one kind into who it is for: which user, which
|
|
9
|
+
// client identity. Verifiers are the device's, set on the class before boot
|
|
10
|
+
// the way an envoy reads a secret from the device: never through cells.
|
|
11
|
+
import { Being, isSilence, isWord, wordOf } from '@quo-systems/quo';
|
|
12
|
+
export class Desk extends Being {
|
|
13
|
+
// proof kind -> verifier. The route that makes a proof kind registers the
|
|
14
|
+
// verifier for it; the desk knows no kind by itself.
|
|
15
|
+
static verifiers = {};
|
|
16
|
+
static asks = {
|
|
17
|
+
hello: { description: 'how to get in', input: { type: 'object' } },
|
|
18
|
+
device: { description: 'trade a proof for a device invitation', input: { type: 'object', properties: { proof: { type: 'object' } }, required: ['proof'] } },
|
|
19
|
+
};
|
|
20
|
+
hello(_args, asker) {
|
|
21
|
+
return { desk: true, asker: asker.id ?? null, kinds: Object.keys(Desk.verifiers), users: Object.keys(this.cells.standings).filter((k) => k.startsWith('user:')).map((k) => k.slice(5)) };
|
|
22
|
+
}
|
|
23
|
+
async device(args) {
|
|
24
|
+
const proof = args.proof;
|
|
25
|
+
if (!proof || typeof proof !== 'object' || Array.isArray(proof) || typeof proof.kind !== 'string')
|
|
26
|
+
return { error: 'proof has a kind' };
|
|
27
|
+
const verify = Object.hasOwn(Desk.verifiers, proof.kind) ? Desk.verifiers[proof.kind] : undefined;
|
|
28
|
+
if (typeof verify !== 'function')
|
|
29
|
+
return { error: 'no such proof kind' };
|
|
30
|
+
const who = await verify(proof);
|
|
31
|
+
if (!who)
|
|
32
|
+
return { error: 'refused' };
|
|
33
|
+
const user = this.standings[`user:${who.user}`];
|
|
34
|
+
if (!user)
|
|
35
|
+
return { error: 'no such user' };
|
|
36
|
+
const inv = await user.ask('device', who.wake === true ? { client: who.client, wake: true } : { client: who.client });
|
|
37
|
+
if (isSilence(inv))
|
|
38
|
+
return { error: 'silence' };
|
|
39
|
+
if (isWord(inv))
|
|
40
|
+
return { error: wordOf(inv) };
|
|
41
|
+
return inv;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { DESK } from './user.js';
|
|
2
|
+
export async function setup(hosted, user) {
|
|
3
|
+
if (!/^[\w.-]+$/.test(user) || user === 'desk')
|
|
4
|
+
throw new Error(`a user is a word, and not desk: ${user}`);
|
|
5
|
+
const boot = async (args) => {
|
|
6
|
+
const out = (await hosted.ask('boot', args));
|
|
7
|
+
if (out.error)
|
|
8
|
+
throw new Error(`init: ${out.error}`);
|
|
9
|
+
};
|
|
10
|
+
await boot({ key: 'desk', class: 'Desk', public: true });
|
|
11
|
+
await boot({ key: user, class: 'User' });
|
|
12
|
+
const inv = (await hosted.ask('invite', { being: user, id: DESK }));
|
|
13
|
+
const placed = (await hosted.ask('knock', { being: 'desk', id: `user:${user}`, invitation: inv, method: 'hello' }));
|
|
14
|
+
if (placed.taken !== `user:${user}`)
|
|
15
|
+
throw new Error('init: the desk could not reach the user being');
|
|
16
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Answer, Json, JsonObject, WordName } from '@quo-systems/quo';
|
|
2
|
+
import type { Avatar } from './avatar.ts';
|
|
3
|
+
export type Serving = {
|
|
4
|
+
close(): Promise<void>;
|
|
5
|
+
};
|
|
6
|
+
export type Side = (avatar: Avatar) => Promise<Serving>;
|
|
7
|
+
export type Word = {
|
|
8
|
+
word: 'object';
|
|
9
|
+
value: Json;
|
|
10
|
+
} | {
|
|
11
|
+
word: 'error';
|
|
12
|
+
value: JsonObject;
|
|
13
|
+
} | {
|
|
14
|
+
word: 'silence';
|
|
15
|
+
} | {
|
|
16
|
+
word: 'unreached';
|
|
17
|
+
} | {
|
|
18
|
+
word: 'quo';
|
|
19
|
+
name: WordName;
|
|
20
|
+
};
|
|
21
|
+
export declare function word(answer: Answer): Word;
|
|
22
|
+
export declare const SILENCE_TEXT = "silence: the work may have happened, and no answer came back";
|
|
23
|
+
export declare const UNREACHED_TEXT = "unreached: nothing was delivered; asking again is safe";
|
|
24
|
+
export declare const WORD_TEXT: Record<WordName, string>;
|
|
25
|
+
export declare const wordText: (name: WordName) => string;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// The side: the one interface an avatar speaks through to someone outside
|
|
3
|
+
// Quo. A side takes the avatar object, in-process, and renders three things
|
|
4
|
+
// its own way: her describe as what that someone reads, a call as an ask on
|
|
5
|
+
// her one standing, and a push as a notification. Sides in this repository:
|
|
6
|
+
// a screen, an MCP server, a runner, an event-driven agent. None knows
|
|
7
|
+
// another exists, and none of this file names a platform.
|
|
8
|
+
import { isSilence, isUnreached, isWord, wordOf } from '@quo-systems/quo';
|
|
9
|
+
export function word(answer) {
|
|
10
|
+
if (isSilence(answer))
|
|
11
|
+
return { word: 'silence' };
|
|
12
|
+
if (isUnreached(answer))
|
|
13
|
+
return { word: 'unreached' };
|
|
14
|
+
if (isWord(answer))
|
|
15
|
+
return { word: 'quo', name: wordOf(answer) };
|
|
16
|
+
if (answer !== null && typeof answer === 'object' && !Array.isArray(answer) && 'error' in answer)
|
|
17
|
+
return { word: 'error', value: answer };
|
|
18
|
+
return { word: 'object', value: answer };
|
|
19
|
+
}
|
|
20
|
+
// What a side says about the two words that carry nothing, so that whoever
|
|
21
|
+
// reads them can decide: silence promises nothing, unreached promises that
|
|
22
|
+
// nothing was delivered.
|
|
23
|
+
export const SILENCE_TEXT = 'silence: the work may have happened, and no answer came back';
|
|
24
|
+
export const UNREACHED_TEXT = 'unreached: nothing was delivered; asking again is safe';
|
|
25
|
+
// And the ward's other words, each with what a side says of it.
|
|
26
|
+
export const WORD_TEXT = {
|
|
27
|
+
unreached: UNREACHED_TEXT,
|
|
28
|
+
late: 'late: the wait ran out; the work may have happened',
|
|
29
|
+
invitation: 'invitation: that is not an invitation',
|
|
30
|
+
dropped: 'dropped: the standing is gone',
|
|
31
|
+
removed: 'removed: the far being removed you',
|
|
32
|
+
absent: 'absent: the far being did not come back this run',
|
|
33
|
+
unannounced: 'unannounced: the knock announced no key of its own',
|
|
34
|
+
repeated: 'repeated: that number was already honoured',
|
|
35
|
+
threw: 'threw: the far being threw',
|
|
36
|
+
};
|
|
37
|
+
export const wordText = (name) => WORD_TEXT[name];
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { Being } from '@quo-systems/quo';
|
|
2
|
+
import type { Asker, JsonObject, OccupantRecord } from '@quo-systems/quo';
|
|
3
|
+
export declare const DESK = "desk";
|
|
4
|
+
declare const isDesk: (occ: OccupantRecord | undefined) => boolean;
|
|
5
|
+
declare const isDevice: (occ: OccupantRecord | undefined) => boolean;
|
|
6
|
+
declare const mayWake: (occ: OccupantRecord | undefined) => boolean;
|
|
7
|
+
export declare class User extends Being {
|
|
8
|
+
static cells: {
|
|
9
|
+
name: string;
|
|
10
|
+
reports: JsonObject[];
|
|
11
|
+
};
|
|
12
|
+
static asks: {
|
|
13
|
+
hello: {
|
|
14
|
+
description: string;
|
|
15
|
+
input: {
|
|
16
|
+
type: string;
|
|
17
|
+
properties: {
|
|
18
|
+
invitation: {
|
|
19
|
+
type: string;
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
whoami: {
|
|
25
|
+
description: string;
|
|
26
|
+
input: {
|
|
27
|
+
type: string;
|
|
28
|
+
};
|
|
29
|
+
for: typeof isDevice;
|
|
30
|
+
};
|
|
31
|
+
device: {
|
|
32
|
+
description: string;
|
|
33
|
+
input: {
|
|
34
|
+
type: string;
|
|
35
|
+
properties: {
|
|
36
|
+
client: {
|
|
37
|
+
type: string;
|
|
38
|
+
};
|
|
39
|
+
wake: {
|
|
40
|
+
type: string;
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
required: string[];
|
|
44
|
+
};
|
|
45
|
+
for: typeof isDesk;
|
|
46
|
+
};
|
|
47
|
+
push: {
|
|
48
|
+
description: string;
|
|
49
|
+
input: {
|
|
50
|
+
type: string;
|
|
51
|
+
properties: {
|
|
52
|
+
client: {
|
|
53
|
+
type: string;
|
|
54
|
+
};
|
|
55
|
+
object: {
|
|
56
|
+
type: string;
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
required: string[];
|
|
60
|
+
};
|
|
61
|
+
for: typeof mayWake;
|
|
62
|
+
};
|
|
63
|
+
chores: {
|
|
64
|
+
description: string;
|
|
65
|
+
input: {
|
|
66
|
+
type: string;
|
|
67
|
+
};
|
|
68
|
+
for: (occ: OccupantRecord | undefined) => boolean;
|
|
69
|
+
};
|
|
70
|
+
report: {
|
|
71
|
+
description: string;
|
|
72
|
+
input: {
|
|
73
|
+
type: string;
|
|
74
|
+
properties: {
|
|
75
|
+
event: {
|
|
76
|
+
type: string;
|
|
77
|
+
};
|
|
78
|
+
result: {};
|
|
79
|
+
};
|
|
80
|
+
required: string[];
|
|
81
|
+
};
|
|
82
|
+
for: typeof isDevice;
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
hello(args: JsonObject, asker: Asker): Promise<{
|
|
86
|
+
welcome: string | null;
|
|
87
|
+
name: import("@quo-systems/quo").Json;
|
|
88
|
+
}>;
|
|
89
|
+
whoami(_args: JsonObject, asker: Asker): {
|
|
90
|
+
id: string | null;
|
|
91
|
+
client: string | null;
|
|
92
|
+
};
|
|
93
|
+
device(args: JsonObject): Promise<JsonObject>;
|
|
94
|
+
push(args: JsonObject): Promise<{
|
|
95
|
+
error: string;
|
|
96
|
+
pushed?: undefined;
|
|
97
|
+
} | {
|
|
98
|
+
error?: undefined;
|
|
99
|
+
pushed: import("@quo-systems/quo").Json;
|
|
100
|
+
}>;
|
|
101
|
+
chores(): {
|
|
102
|
+
chores: string[];
|
|
103
|
+
};
|
|
104
|
+
report(args: JsonObject, asker: Asker): {
|
|
105
|
+
reported: boolean;
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
export {};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// The user being. One per human, running wherever the human's world runs.
|
|
3
|
+
// She holds the human's standings, and everyone who acts for the human is
|
|
4
|
+
// her occupant, seeing what her gate shows. Two of her occupants are
|
|
5
|
+
// special only by the id the root chose for them: `desk`, the front desk,
|
|
6
|
+
// who may ask her to invite a device; and each device, whose id is the
|
|
7
|
+
// client identity the desk established.
|
|
8
|
+
import { Being, isSilence, isWord, wordOf } from '@quo-systems/quo';
|
|
9
|
+
export const DESK = 'desk';
|
|
10
|
+
const isDesk = (occ) => occ?.id === DESK;
|
|
11
|
+
const isDevice = (occ) => occ !== undefined && occ.id !== DESK;
|
|
12
|
+
const client = (occ) => (typeof occ?.notes.client === 'string' ? occ.notes.client : null);
|
|
13
|
+
// A device the human allowed to wake her other devices: the note says so.
|
|
14
|
+
const mayWake = (occ) => isDesk(occ) || occ?.notes.wake === true;
|
|
15
|
+
export class User extends Being {
|
|
16
|
+
static cells = { name: '', reports: [] };
|
|
17
|
+
static asks = {
|
|
18
|
+
hello: { description: 'say hello, and hand back an invitation so she can reach you', input: { type: 'object', properties: { invitation: { type: 'object' } } } },
|
|
19
|
+
whoami: { description: 'who she thinks you are', input: { type: 'object' }, for: isDevice },
|
|
20
|
+
device: { description: 'mint an invitation for a device', input: { type: 'object', properties: { client: { type: 'string' }, wake: { type: 'boolean' } }, required: ['client'] }, for: isDesk },
|
|
21
|
+
push: { description: 'push an object to a device: wake it with an event', input: { type: 'object', properties: { client: { type: 'string' }, object: { type: 'object' } }, required: ['client', 'object'] }, for: mayWake },
|
|
22
|
+
chores: { description: 'what the agent may run', input: { type: 'object' }, for: (occ) => client(occ) === 'agent' },
|
|
23
|
+
report: { description: 'what a run of yours found', input: { type: 'object', properties: { event: { type: 'object' }, result: {} }, required: ['event', 'result'] }, for: isDevice },
|
|
24
|
+
};
|
|
25
|
+
// Anyone may say hello. A device that hands her an invitation in the args
|
|
26
|
+
// is taken as a standing under its own id, so she can push to it later.
|
|
27
|
+
async hello(args, asker) {
|
|
28
|
+
if (asker.id !== undefined && args.invitation && typeof args.invitation === 'object' && !Array.isArray(args.invitation)) {
|
|
29
|
+
const inv = args.invitation;
|
|
30
|
+
const back = await this.knock(inv);
|
|
31
|
+
if (!isSilence(back) && !isWord(back))
|
|
32
|
+
await this.take(`to:${asker.id}`, inv);
|
|
33
|
+
}
|
|
34
|
+
return { welcome: asker.id ?? null, name: this.cells.name };
|
|
35
|
+
}
|
|
36
|
+
whoami(_args, asker) {
|
|
37
|
+
return { id: asker.id ?? null, client: client(this.occupant(asker)) };
|
|
38
|
+
}
|
|
39
|
+
// The front desk asks; she mints. The client identity goes into the
|
|
40
|
+
// occupant's notes, and that is what every gate reads. `wake` is the
|
|
41
|
+
// human's word at the exchange that this device may wake her others.
|
|
42
|
+
async device(args) {
|
|
43
|
+
const c = typeof args.client === 'string' ? args.client : null;
|
|
44
|
+
if (c === null)
|
|
45
|
+
return { error: 'client is a string' };
|
|
46
|
+
const inv = await this.invite(c);
|
|
47
|
+
if (inv === null)
|
|
48
|
+
return { error: 'id taken' };
|
|
49
|
+
const rec = this.cells.occupants[c];
|
|
50
|
+
if (rec) {
|
|
51
|
+
rec.notes.client = c;
|
|
52
|
+
if (args.wake === true)
|
|
53
|
+
rec.notes.wake = true;
|
|
54
|
+
}
|
|
55
|
+
return inv;
|
|
56
|
+
}
|
|
57
|
+
async push(args) {
|
|
58
|
+
const c = typeof args.client === 'string' ? args.client : null;
|
|
59
|
+
const st = c === null ? undefined : this.standings[`to:${c}`];
|
|
60
|
+
if (!st)
|
|
61
|
+
return { error: 'no such device, or it gave no way back' };
|
|
62
|
+
const out = await st.ask('notify', args.object ?? {});
|
|
63
|
+
return isSilence(out) ? { error: 'silence' } : isWord(out) ? { error: wordOf(out) } : { pushed: out };
|
|
64
|
+
}
|
|
65
|
+
chores() {
|
|
66
|
+
return { chores: ['census', 'report'] };
|
|
67
|
+
}
|
|
68
|
+
// A device that ran something for her says what it found. Kept, so that
|
|
69
|
+
// whoever renders her can show it; the ask itself is the callback.
|
|
70
|
+
report(args, asker) {
|
|
71
|
+
this.cells.reports.push({ from: asker.id ?? null, event: args.event ?? null, result: args.result ?? null });
|
|
72
|
+
return { reported: true };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type Answer = {
|
|
2
|
+
result: unknown;
|
|
3
|
+
} | {
|
|
4
|
+
error: string;
|
|
5
|
+
};
|
|
6
|
+
export declare function ask(dir: string, method: string | undefined, args?: Record<string, unknown>, ward?: string, via?: string): Promise<Answer>;
|
|
7
|
+
export declare function reach(dir: string, pk: string, url: string): Promise<Answer>;
|
|
8
|
+
export type Run = {
|
|
9
|
+
url: string;
|
|
10
|
+
model: string;
|
|
11
|
+
key?: string;
|
|
12
|
+
turns?: number;
|
|
13
|
+
};
|
|
14
|
+
export declare function side(dir: string, as: string, ward?: string, run?: Run): Promise<void>;
|