pi-roundtable-web 0.8.0 → 0.9.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/CHANGELOG.md +8 -0
- package/README.md +71 -33
- package/dist/chunk-0vqnvf92.js +36 -0
- package/dist/index.html +1 -1
- package/package.json +4 -4
- package/src/api-types.ts +22 -2
- package/src/cloudflare-access.ts +35 -9
- package/src/console-api.ts +105 -26
- package/src/console-server.ts +35 -13
- package/src/index.ts +7 -2
- package/src/options.ts +11 -5
- package/src/verifier.ts +23 -5
- package/src/visitors.ts +89 -0
- package/src/web-plugin.ts +18 -5
- package/dist/chunk-2cm6wak5.js +0 -36
package/src/visitors.ts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ActorFacts,
|
|
3
|
+
IdentityService,
|
|
4
|
+
Logger,
|
|
5
|
+
Principal,
|
|
6
|
+
} from "pi-roundtable";
|
|
7
|
+
|
|
8
|
+
/** Who a request the console admitted comes from: an owner's principal. */
|
|
9
|
+
export interface Visitor {
|
|
10
|
+
principal: Principal;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** The visitor of a request, or why it is refused, for the operator's log. */
|
|
14
|
+
export type Identified = { visitor: Visitor } | { refusal: string };
|
|
15
|
+
|
|
16
|
+
/** The reads of `IDENTITY` the console uses; none of them makes or changes a principal. */
|
|
17
|
+
export type ConsoleIdentity = Pick<
|
|
18
|
+
IdentityService,
|
|
19
|
+
"principalOf" | "principal" | "tierOf" | "owners"
|
|
20
|
+
>;
|
|
21
|
+
|
|
22
|
+
export interface VisitorOptions {
|
|
23
|
+
/** Whom a request from a verifier that reports no actor stands for; default the primary owner. */
|
|
24
|
+
ownerId?: string;
|
|
25
|
+
logger: Logger;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** The provider of the aliases 0.9 keeps for 0.8 ids; `IDENTITY.resolve` refuses it as no surface's. */
|
|
29
|
+
const LEGACY_PROVIDER = "legacy";
|
|
30
|
+
|
|
31
|
+
/** How many unlinked identities are remembered, so each is warned about once. */
|
|
32
|
+
const WARNED_MAX = 100;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Identifies the person behind each request the verifier admitted, by reading `IDENTITY` only, so
|
|
36
|
+
* a visitor never makes a principal: the principal their identity is linked to, who must hold the
|
|
37
|
+
* owner role. An identity linked to no one is refused on every host, however many owners it has:
|
|
38
|
+
* the verifier may vouch for members too, so only a link says whose an identity is. A verifier
|
|
39
|
+
* that reports no actor speaks for `ownerId`, default the primary owner, as in 0.8.
|
|
40
|
+
*/
|
|
41
|
+
export function consoleVisitors(
|
|
42
|
+
identity: ConsoleIdentity,
|
|
43
|
+
options: VisitorOptions,
|
|
44
|
+
): (actor: ActorFacts | undefined) => Promise<Identified> {
|
|
45
|
+
const { logger } = options;
|
|
46
|
+
const warned = new Set<string>();
|
|
47
|
+
const warnOnce = (key: string, message: string) => {
|
|
48
|
+
if (warned.has(key) || warned.size >= WARNED_MAX) return;
|
|
49
|
+
warned.add(key);
|
|
50
|
+
logger.warn(`web-console: ${message}`);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const owner = async (id: string): Promise<Identified> => {
|
|
54
|
+
const principal = await identity.principal(id);
|
|
55
|
+
if (!principal) return { refusal: `there is no principal ${id}` };
|
|
56
|
+
if (principal.disabled) return { refusal: `principal ${id} is disabled` };
|
|
57
|
+
if ((await identity.tierOf(id)) !== "owner")
|
|
58
|
+
return { refusal: `principal ${id} holds no owner role` };
|
|
59
|
+
return { visitor: { principal } };
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
return async (actor) => {
|
|
63
|
+
if (!actor) {
|
|
64
|
+
warnOnce(
|
|
65
|
+
"",
|
|
66
|
+
"the verifier reports no actor, so the console takes every request it admits as the primary owner's. Have it report who signed in (cloudflareAccess does)",
|
|
67
|
+
);
|
|
68
|
+
const id = options.ownerId ?? (await identity.owners())[0]?.id;
|
|
69
|
+
return id === undefined
|
|
70
|
+
? { refusal: "the host has no owner" }
|
|
71
|
+
: owner(id);
|
|
72
|
+
}
|
|
73
|
+
const named = `${actor.provider}:${actor.subject}`;
|
|
74
|
+
if (actor.provider === LEGACY_PROVIDER)
|
|
75
|
+
return { refusal: `${named} is a 0.8 id, not an identity` };
|
|
76
|
+
let id: string | undefined;
|
|
77
|
+
try {
|
|
78
|
+
id = await identity.principalOf(named);
|
|
79
|
+
} catch {
|
|
80
|
+
return { refusal: "the verifier's actor is no identity" };
|
|
81
|
+
}
|
|
82
|
+
if (id !== undefined) return owner(id);
|
|
83
|
+
warnOnce(
|
|
84
|
+
named,
|
|
85
|
+
`${named} is linked to no principal, so the console refuses it. If it is an owner's, add "${named}" to that owner's access.owners[].identities, or run: roundtable principal link <owner's principal id> ${named}`,
|
|
86
|
+
);
|
|
87
|
+
return { refusal: `${named} is linked to no principal` };
|
|
88
|
+
};
|
|
89
|
+
}
|
package/src/web-plugin.ts
CHANGED
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
AGENTS,
|
|
3
3
|
CONVERSATIONS,
|
|
4
4
|
definePlugin,
|
|
5
|
+
IDENTITY,
|
|
5
6
|
MEMORY,
|
|
6
7
|
type RoundtablePlugin,
|
|
7
8
|
} from "pi-roundtable";
|
|
@@ -14,17 +15,20 @@ import {
|
|
|
14
15
|
resolveOptions,
|
|
15
16
|
type WebConsoleOptions,
|
|
16
17
|
} from "./options.ts";
|
|
18
|
+
import { consoleVisitors } from "./visitors.ts";
|
|
17
19
|
|
|
18
20
|
/**
|
|
19
|
-
* The
|
|
20
|
-
*
|
|
21
|
-
*
|
|
21
|
+
* The owners' web console: conversations, transcripts, memory notes, and the agent team, served
|
|
22
|
+
* on a route of the host's listener and updated live. Every request needs the verifier to vouch
|
|
23
|
+
* for the person and `IDENTITY` to name them an owner. The options are checked here, so a missing
|
|
24
|
+
* verifier or a bad setting throws when the config is loaded.
|
|
22
25
|
*/
|
|
23
26
|
export function webConsole(options: WebConsoleOptions): RoundtablePlugin {
|
|
24
27
|
const settings = resolveOptions(options);
|
|
25
28
|
return definePlugin({
|
|
26
29
|
name: "web-console",
|
|
27
30
|
requires: [
|
|
31
|
+
IDENTITY,
|
|
28
32
|
...(settings.panes.includes("overview") ? [AGENTS] : []),
|
|
29
33
|
...(settings.panes.includes("notes") ? [MEMORY] : []),
|
|
30
34
|
],
|
|
@@ -48,6 +52,7 @@ async function build(settings: ResolvedOptions, context: Context) {
|
|
|
48
52
|
const connection = services.find(DISCORD)?.connection;
|
|
49
53
|
// Absent on a host without the registry: the console then lists conversations found by name only.
|
|
50
54
|
const registry = services.find(CONVERSATIONS);
|
|
55
|
+
const identity = services.get(IDENTITY);
|
|
51
56
|
const listeners: (() => void)[] = [];
|
|
52
57
|
const changed = () => {
|
|
53
58
|
for (const listener of listeners) listener();
|
|
@@ -64,9 +69,13 @@ async function build(settings: ResolvedOptions, context: Context) {
|
|
|
64
69
|
...(connection
|
|
65
70
|
? { channelName: (id: string) => connection.channelInfo(id) }
|
|
66
71
|
: {}),
|
|
67
|
-
...(settings.
|
|
68
|
-
? {
|
|
72
|
+
...(settings.panes.includes("notes")
|
|
73
|
+
? {
|
|
74
|
+
memoryOf: (principalId: string) =>
|
|
75
|
+
services.get(MEMORY).forSpeaker(principalId),
|
|
76
|
+
}
|
|
69
77
|
: {}),
|
|
78
|
+
people: identity,
|
|
70
79
|
...(settings.exclude ? { exclude: settings.exclude } : {}),
|
|
71
80
|
...(registry ? { registry } : {}),
|
|
72
81
|
relayNotes: settings.relayNotes,
|
|
@@ -80,6 +89,10 @@ async function build(settings: ResolvedOptions, context: Context) {
|
|
|
80
89
|
mount: settings.mount,
|
|
81
90
|
assets,
|
|
82
91
|
verifier: settings.verifier,
|
|
92
|
+
identify: consoleVisitors(identity, {
|
|
93
|
+
logger,
|
|
94
|
+
...(settings.ownerId ? { ownerId: settings.ownerId } : {}),
|
|
95
|
+
}),
|
|
83
96
|
origin: settings.origin,
|
|
84
97
|
pathRouting: settings.routing === "path",
|
|
85
98
|
...(settings.presentation ? { presentation: settings.presentation } : {}),
|