@botiverse/porthole 0.1.2 → 0.1.4
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/dist/annotate/types.d.ts +120 -0
- package/dist/annotate/types.js +42 -0
- package/dist/annotate/validate.d.ts +11 -0
- package/dist/annotate/validate.js +156 -0
- package/dist/bridge/bridge.d.ts +4 -0
- package/dist/bridge/bridge.js +12 -1
- package/dist/compile/policy.d.ts +10 -3
- package/dist/compile/policy.js +16 -5
- package/dist/config/capabilities.d.ts +37 -4
- package/dist/config/capabilities.js +20 -0
- package/dist/config/schema.d.ts +11 -0
- package/dist/config/validate.js +12 -0
- package/dist/conformance/clipboard.d.ts +2 -0
- package/dist/conformance/clipboard.js +167 -0
- package/dist/conformance/kit.d.ts +1 -1
- package/dist/conformance/probes.js +2 -0
- package/dist/conformance/served.d.ts +8 -1
- package/dist/conformance/served.js +12 -8
- package/dist/guide/guide.js +11 -5
- package/dist/host/clipboard.d.ts +11 -0
- package/dist/host/clipboard.js +43 -0
- package/dist/host/create-host.js +165 -7
- package/dist/host/types.d.ts +68 -2
- package/dist/index.d.ts +8 -5
- package/dist/index.js +7 -3
- package/dist/mcp/adapter.js +10 -1
- package/dist/presets/presets.d.ts +3 -1
- package/dist/presets/presets.js +1 -0
- package/dist/protocol/mcp-apps.d.ts +5 -0
- package/dist/protocol/mcp-apps.js +5 -0
- package/dist/protocol/shell-adapter.js +6 -2
- package/dist/react/dialog.js +2 -0
- package/dist/react/frame.d.ts +50 -2
- package/dist/react/frame.js +153 -11
- package/dist/react/index.d.ts +2 -1
- package/dist/react/index.js +2 -1
- package/dist/react/toast.d.ts +33 -0
- package/dist/react/toast.js +42 -0
- package/dist/server/naming.d.ts +6 -0
- package/dist/server/naming.js +38 -0
- package/dist/server/pages.js +2 -0
- package/dist/server/server.js +49 -17
- package/dist/server/types.d.ts +6 -3
- package/dist/shell/annotate.d.ts +20 -0
- package/dist/shell/annotate.js +700 -0
- package/dist/shell/document.d.ts +6 -0
- package/dist/shell/document.js +12 -3
- package/dist/shell/plan.d.ts +2 -0
- package/dist/shell/plan.js +2 -1
- package/dist/shell/runtime.d.ts +4 -1
- package/dist/shell/runtime.js +71 -3
- package/package.json +1 -1
- package/src/annotate/types.ts +126 -0
- package/src/annotate/validate.ts +186 -0
- package/src/bridge/bridge.ts +13 -0
- package/src/compile/policy.ts +17 -5
- package/src/config/capabilities.ts +33 -3
- package/src/config/schema.ts +12 -0
- package/src/config/validate.ts +15 -0
- package/src/conformance/clipboard.ts +184 -0
- package/src/conformance/kit.ts +1 -1
- package/src/conformance/probes.ts +2 -0
- package/src/conformance/served.ts +24 -9
- package/src/guide/guide.ts +22 -5
- package/src/host/clipboard.ts +41 -0
- package/src/host/create-host.ts +181 -7
- package/src/host/types.ts +64 -2
- package/src/index.ts +24 -0
- package/src/mcp/adapter.ts +10 -1
- package/src/presets/presets.ts +4 -0
- package/src/protocol/mcp-apps.ts +6 -0
- package/src/protocol/shell-adapter.ts +11 -2
- package/src/react/dialog.tsx +2 -0
- package/src/react/frame.tsx +231 -13
- package/src/react/index.ts +9 -1
- package/src/react/toast.tsx +96 -0
- package/src/server/naming.ts +41 -0
- package/src/server/pages.ts +2 -0
- package/src/server/server.ts +61 -20
- package/src/server/types.ts +6 -3
- package/src/shell/annotate.ts +700 -0
- package/src/shell/document.ts +21 -3
- package/src/shell/plan.ts +4 -1
- package/src/shell/runtime.ts +71 -3
package/dist/host/create-host.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { ANNOTATE_METHODS, } from "../annotate/types.js";
|
|
2
|
+
import { annotateMode, checkMarks, parseReport } from "../annotate/validate.js";
|
|
1
3
|
import { createBridge } from "../bridge/bridge.js";
|
|
2
4
|
import { compile } from "../compile/compile.js";
|
|
3
5
|
import { declaredCapabilities } from "../compile/policy.js";
|
|
@@ -13,6 +15,14 @@ const DEFAULT_LOAD_TIMEOUT_MS = 15_000;
|
|
|
13
15
|
/** How many times a display is started again after its content navigated its frame away. */
|
|
14
16
|
const MAX_RESTARTS = 3;
|
|
15
17
|
const BASE_KEYS = new Set(["hostContextChanged", "teardown", "close"]);
|
|
18
|
+
/** The frame's annotation reports, by method. */
|
|
19
|
+
const REPORTS = {
|
|
20
|
+
[ANNOTATE_METHODS.selected]: "selected",
|
|
21
|
+
[ANNOTATE_METHODS.unselected]: "unselected",
|
|
22
|
+
[ANNOTATE_METHODS.cancelled]: "cancelled",
|
|
23
|
+
[ANNOTATE_METHODS.anchored]: "anchored",
|
|
24
|
+
};
|
|
25
|
+
const isRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
16
26
|
const STANDARD = new Set(MCP_APPS_STYLE_VARIABLES);
|
|
17
27
|
/**
|
|
18
28
|
* Compiles `config` once and returns a host that mounts displays under it. Throws a `ConfigError`
|
|
@@ -47,6 +57,16 @@ export function createHost(config, options = {}) {
|
|
|
47
57
|
listener(detail);
|
|
48
58
|
}
|
|
49
59
|
};
|
|
60
|
+
// What the content did wrong (the agent's to hear and fix), and what failed on the host's
|
|
61
|
+
// side (its own to handle), each on its own event and on `error` with its source.
|
|
62
|
+
const contentError = (message) => {
|
|
63
|
+
emit("contentError", { message });
|
|
64
|
+
emit("error", { message, source: "content" });
|
|
65
|
+
};
|
|
66
|
+
const hostError = (message, kind) => {
|
|
67
|
+
emit("hostError", { message, kind });
|
|
68
|
+
emit("error", { message, source: "host" });
|
|
69
|
+
};
|
|
50
70
|
let scheme = mount.scheme;
|
|
51
71
|
let height = mount.initialHeightPx ?? 0;
|
|
52
72
|
let surface = null;
|
|
@@ -60,6 +80,125 @@ export function createHost(config, options = {}) {
|
|
|
60
80
|
const view = container.ownerDocument.defaultView;
|
|
61
81
|
// The adapter's own operations, the last call of each, replayed into every new frame.
|
|
62
82
|
const replay = new Map();
|
|
83
|
+
// Annotations: the host's state, given to each frame once it finished its handshake.
|
|
84
|
+
const annotates = config.capabilities.annotate !== undefined;
|
|
85
|
+
let annotating = null;
|
|
86
|
+
let marks = [];
|
|
87
|
+
let activeMark = null;
|
|
88
|
+
let handshaken = false;
|
|
89
|
+
const toFrame = (method, params) => {
|
|
90
|
+
if (handshaken && surface !== null) {
|
|
91
|
+
surface.transport.send({ jsonrpc: "2.0", method, params });
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
const giveAnnotations = () => {
|
|
95
|
+
if (marks.length > 0) {
|
|
96
|
+
toFrame(ANNOTATE_METHODS.show, {
|
|
97
|
+
marks,
|
|
98
|
+
...(activeMark === null ? {} : { active: activeMark }),
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
if (annotating !== null) {
|
|
102
|
+
toFrame(ANNOTATE_METHODS.start, { ...annotating });
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
/**
|
|
106
|
+
* A report from the frame: checked, taken only while the host asked for it (a selection
|
|
107
|
+
* only in select mode, results only for its own marks), then through the bridge's gates.
|
|
108
|
+
*/
|
|
109
|
+
const onAnnotation = (raw, bridge) => {
|
|
110
|
+
if (!isRecord(raw) ||
|
|
111
|
+
raw.jsonrpc !== "2.0" ||
|
|
112
|
+
"id" in raw ||
|
|
113
|
+
typeof raw.method !== "string") {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const type = REPORTS[raw.method];
|
|
117
|
+
if (type === undefined) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
let report = parseReport({ ...(isRecord(raw.params) ? raw.params : {}), type });
|
|
121
|
+
if (report === null) {
|
|
122
|
+
void bridge.call("annotate", null);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
switch (report.type) {
|
|
126
|
+
case "selected": {
|
|
127
|
+
const kind = report.anchor.kind;
|
|
128
|
+
if (annotating === null ||
|
|
129
|
+
!(kind === "element" ? annotating.elements : annotating.text)) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (kind === "element") {
|
|
133
|
+
annotating = annotating.text ? { elements: false, text: true } : null;
|
|
134
|
+
}
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
case "unselected":
|
|
138
|
+
if (annotating?.text !== true) {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
break;
|
|
142
|
+
case "cancelled":
|
|
143
|
+
if (annotating?.elements !== true) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
annotating = annotating.text ? { elements: false, text: true } : null;
|
|
147
|
+
break;
|
|
148
|
+
case "anchored": {
|
|
149
|
+
const known = new Set(marks.map((mark) => mark.id));
|
|
150
|
+
const results = report.results.filter((result) => known.has(result.id));
|
|
151
|
+
if (results.length === 0) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
report = { type: "anchored", results };
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
void bridge.call("annotate", report);
|
|
159
|
+
};
|
|
160
|
+
const needsAnnotate = (operation) => {
|
|
161
|
+
if (!annotates) {
|
|
162
|
+
throw new TypeError(`annotations.${operation}: the config doesn't declare annotate`);
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
const annotations = {
|
|
166
|
+
start(mode) {
|
|
167
|
+
needsAnnotate("start");
|
|
168
|
+
annotating = annotateMode(mode);
|
|
169
|
+
toFrame(ANNOTATE_METHODS.start, { ...annotating });
|
|
170
|
+
},
|
|
171
|
+
stop() {
|
|
172
|
+
needsAnnotate("stop");
|
|
173
|
+
annotating = null;
|
|
174
|
+
toFrame(ANNOTATE_METHODS.stop, {});
|
|
175
|
+
},
|
|
176
|
+
show(next) {
|
|
177
|
+
needsAnnotate("show");
|
|
178
|
+
marks = checkMarks(next);
|
|
179
|
+
if (activeMark !== null && !marks.some((mark) => mark.id === activeMark)) {
|
|
180
|
+
activeMark = null;
|
|
181
|
+
}
|
|
182
|
+
toFrame(ANNOTATE_METHODS.show, {
|
|
183
|
+
marks,
|
|
184
|
+
...(activeMark === null ? {} : { active: activeMark }),
|
|
185
|
+
});
|
|
186
|
+
},
|
|
187
|
+
focus(id) {
|
|
188
|
+
needsAnnotate("focus");
|
|
189
|
+
if (!marks.some((mark) => mark.id === id)) {
|
|
190
|
+
throw new TypeError(`annotations.focus: no mark ${JSON.stringify(id)} is shown`);
|
|
191
|
+
}
|
|
192
|
+
activeMark = id;
|
|
193
|
+
toFrame(ANNOTATE_METHODS.focus, { id });
|
|
194
|
+
},
|
|
195
|
+
clear() {
|
|
196
|
+
needsAnnotate("clear");
|
|
197
|
+
marks = [];
|
|
198
|
+
activeMark = null;
|
|
199
|
+
toFrame(ANNOTATE_METHODS.clear, {});
|
|
200
|
+
},
|
|
201
|
+
};
|
|
63
202
|
const hostContext = () => {
|
|
64
203
|
const width = container.getBoundingClientRect().width;
|
|
65
204
|
// The spec's names in `styles` (the App SDK refuses others there), the rest in our field.
|
|
@@ -99,7 +238,7 @@ export function createHost(config, options = {}) {
|
|
|
99
238
|
return;
|
|
100
239
|
}
|
|
101
240
|
case "error":
|
|
102
|
-
|
|
241
|
+
contentError(event.message);
|
|
103
242
|
return;
|
|
104
243
|
case "refused":
|
|
105
244
|
emit("refused", { capability: event.capability, refusal: event.refusal });
|
|
@@ -107,9 +246,13 @@ export function createHost(config, options = {}) {
|
|
|
107
246
|
case "muted":
|
|
108
247
|
emit("muted", undefined);
|
|
109
248
|
return;
|
|
249
|
+
case "annotation":
|
|
250
|
+
emit("annotation", event.report);
|
|
251
|
+
return;
|
|
110
252
|
}
|
|
111
253
|
};
|
|
112
254
|
const stop = () => {
|
|
255
|
+
handshaken = false;
|
|
113
256
|
abort?.abort();
|
|
114
257
|
abort = null;
|
|
115
258
|
outcomeWatch?.stop();
|
|
@@ -127,8 +270,9 @@ export function createHost(config, options = {}) {
|
|
|
127
270
|
allow: compiled.allow,
|
|
128
271
|
referrerPolicy: compiled.referrerPolicy,
|
|
129
272
|
colorScheme: scheme,
|
|
130
|
-
// An app reports no size: its frame fills the host's container, at least the minimum
|
|
131
|
-
|
|
273
|
+
// An app reports no size: its frame fills the host's container, at least the minimum;
|
|
274
|
+
// so does any display the host mounts to fill (its content scrolls inside).
|
|
275
|
+
...(speaks && mount.fill !== true
|
|
132
276
|
? { initialHeightPx: height }
|
|
133
277
|
: { fill: true, initialHeightPx: config.limits.minHeightPx }),
|
|
134
278
|
});
|
|
@@ -160,6 +304,9 @@ export function createHost(config, options = {}) {
|
|
|
160
304
|
userActivation: () => created.userActivation(),
|
|
161
305
|
onEvent: onBridge,
|
|
162
306
|
});
|
|
307
|
+
if (annotates) {
|
|
308
|
+
created.transport.listen((raw) => onAnnotation(raw, bridge));
|
|
309
|
+
}
|
|
163
310
|
const opened = adapter.connect({
|
|
164
311
|
display: { id: mount.displayId, title: mount.title },
|
|
165
312
|
compiled,
|
|
@@ -170,6 +317,10 @@ export function createHost(config, options = {}) {
|
|
|
170
317
|
emit: (event) => {
|
|
171
318
|
if (event.type === "initialized") {
|
|
172
319
|
watch.initialized();
|
|
320
|
+
if (!handshaken) {
|
|
321
|
+
handshaken = true;
|
|
322
|
+
giveAnnotations();
|
|
323
|
+
}
|
|
173
324
|
emit("initialized", undefined);
|
|
174
325
|
}
|
|
175
326
|
},
|
|
@@ -187,9 +338,7 @@ export function createHost(config, options = {}) {
|
|
|
187
338
|
}
|
|
188
339
|
else {
|
|
189
340
|
stop();
|
|
190
|
-
|
|
191
|
-
message: "The content kept navigating its frame away; it was stopped.",
|
|
192
|
-
});
|
|
341
|
+
contentError("The content kept navigating its frame away; it was stopped.");
|
|
193
342
|
}
|
|
194
343
|
});
|
|
195
344
|
};
|
|
@@ -205,6 +354,7 @@ export function createHost(config, options = {}) {
|
|
|
205
354
|
html: compiled.shellDocument({
|
|
206
355
|
origins: [container.ownerDocument.defaultView?.location.origin ?? ""],
|
|
207
356
|
title: mount.title,
|
|
357
|
+
...nonceOf(options.nonce),
|
|
208
358
|
...(mount.document === undefined ? {} : { document: mount.document }),
|
|
209
359
|
}),
|
|
210
360
|
});
|
|
@@ -225,13 +375,16 @@ export function createHost(config, options = {}) {
|
|
|
225
375
|
}
|
|
226
376
|
}, (error) => {
|
|
227
377
|
if (mine === generation && !controller.signal.aborted) {
|
|
228
|
-
|
|
378
|
+
// The host's own failure (its backend down, access gone): never the content's.
|
|
379
|
+
hostError(error instanceof Error ? error.message : String(error), "locate");
|
|
380
|
+
emit("outcome", { state: "blocked", reason: "locate" });
|
|
229
381
|
}
|
|
230
382
|
});
|
|
231
383
|
};
|
|
232
384
|
run();
|
|
233
385
|
const controls = {
|
|
234
386
|
displayId: mount.displayId,
|
|
387
|
+
annotations,
|
|
235
388
|
setTheme(next) {
|
|
236
389
|
scheme = next;
|
|
237
390
|
surface?.setColorScheme(next);
|
|
@@ -290,3 +443,8 @@ export function createHost(config, options = {}) {
|
|
|
290
443
|
},
|
|
291
444
|
};
|
|
292
445
|
}
|
|
446
|
+
/** The host's nonce option, read now (`HostOptions.nonce`). */
|
|
447
|
+
function nonceOf(nonce) {
|
|
448
|
+
const value = typeof nonce === "function" ? nonce() : nonce;
|
|
449
|
+
return value === undefined || value === "" ? {} : { nonce: value };
|
|
450
|
+
}
|
package/dist/host/types.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { AnnotateMode, AnnotationMark, AnnotationReport } from "../annotate/types.ts";
|
|
1
2
|
import type { Confirm, CapabilityHandlers, Refusal } from "../bridge/types.ts";
|
|
2
3
|
import type { CompiledConfig } from "../compile/compile.ts";
|
|
3
4
|
import type { CapabilityName } from "../config/capabilities.ts";
|
|
@@ -34,6 +35,13 @@ export interface HostOptions<C extends AdapterConnection = ShellConnection> {
|
|
|
34
35
|
readonly platform?: PlatformHost;
|
|
35
36
|
/** Required for domain and external isolation: where an instance's frame loads from. */
|
|
36
37
|
readonly locator?: InstanceLocator;
|
|
38
|
+
/**
|
|
39
|
+
* The embedding page's CSP nonce, where its `script-src` is nonce-based (P054): an opaque frame
|
|
40
|
+
* (`srcdoc`) inherits the page's policy, so the shell and the content's scripts carry it. A
|
|
41
|
+
* function is read each time a frame starts (a page whose nonce changes). Served frames inherit
|
|
42
|
+
* nothing and need none.
|
|
43
|
+
*/
|
|
44
|
+
readonly nonce?: string | (() => string | undefined);
|
|
37
45
|
}
|
|
38
46
|
export interface MountOptions {
|
|
39
47
|
/** The host's id for this display: what its height is remembered by, and calls name. */
|
|
@@ -49,7 +57,21 @@ export interface MountOptions {
|
|
|
49
57
|
readonly document?: string;
|
|
50
58
|
/** The height to start at, before the content reports its own. */
|
|
51
59
|
readonly initialHeightPx?: number;
|
|
60
|
+
/**
|
|
61
|
+
* Fill the container (give it a height) instead of taking the content's height: the content
|
|
62
|
+
* scrolls inside its frame (an artifact viewer's page). The content's sizes are still reported
|
|
63
|
+
* (`resize`). A url-mode app always fills.
|
|
64
|
+
*/
|
|
65
|
+
readonly fill?: boolean;
|
|
52
66
|
}
|
|
67
|
+
/** What failed on the host's side (`hostError`). */
|
|
68
|
+
export type HostErrorKind =
|
|
69
|
+
/** The locator rejected: the host's backend didn't give a location. */
|
|
70
|
+
"locate"
|
|
71
|
+
/** A display couldn't be made (the React binding: a bad config, a missing handler). */
|
|
72
|
+
| "mount"
|
|
73
|
+
/** An annotation call was refused (an invalid mark, an unknown id). */
|
|
74
|
+
| "annotations";
|
|
53
75
|
/** What a display reports, by event name. */
|
|
54
76
|
/** Why a display's frame showed nothing of its content (`FrameOutcome`). */
|
|
55
77
|
export type FrameBlock =
|
|
@@ -62,7 +84,9 @@ export type FrameBlock =
|
|
|
62
84
|
/** Its app didn't answer. */
|
|
63
85
|
| "upstream"
|
|
64
86
|
/** The host's own check refused it (revoked, no longer a member): `code` says which. */
|
|
65
|
-
| "denied"
|
|
87
|
+
| "denied"
|
|
88
|
+
/** The host's locator failed (its backend down, access gone): see the `hostError` event. */
|
|
89
|
+
| "locate" | "not-found" | "error";
|
|
66
90
|
/**
|
|
67
91
|
* How a display's document went: it loaded (a shell or view finished its handshake; a url-mode
|
|
68
92
|
* app's document loaded), it was blocked (and why), or nothing settled in
|
|
@@ -91,9 +115,28 @@ export interface DisplayEvents {
|
|
|
91
115
|
readonly height: number;
|
|
92
116
|
readonly width?: number;
|
|
93
117
|
};
|
|
94
|
-
/**
|
|
118
|
+
/**
|
|
119
|
+
* What the content did wrong: what it threw, what the sandbox blocked (its own words:
|
|
120
|
+
* untrusted), or that it kept navigating its frame away. The agent's to hear and fix.
|
|
121
|
+
*/
|
|
122
|
+
readonly contentError: {
|
|
123
|
+
readonly message: string;
|
|
124
|
+
};
|
|
125
|
+
/**
|
|
126
|
+
* What failed on the host's side (`kind`): its locator, for now. The host's to handle; never
|
|
127
|
+
* the agent's. The display's `outcome` says `blocked: locate` too.
|
|
128
|
+
*/
|
|
129
|
+
readonly hostError: {
|
|
130
|
+
readonly message: string;
|
|
131
|
+
readonly kind: HostErrorKind;
|
|
132
|
+
};
|
|
133
|
+
/**
|
|
134
|
+
* Both of the above, with their `source`: for a host that listened here before they were
|
|
135
|
+
* split (0.1.3). Prefer `contentError` and `hostError`.
|
|
136
|
+
*/
|
|
95
137
|
readonly error: {
|
|
96
138
|
readonly message: string;
|
|
139
|
+
readonly source: "content" | "host";
|
|
97
140
|
};
|
|
98
141
|
/** The content navigated its frame away; the frame was started again from its content. */
|
|
99
142
|
readonly navigated: undefined;
|
|
@@ -104,6 +147,27 @@ export interface DisplayEvents {
|
|
|
104
147
|
};
|
|
105
148
|
/** Too many declines: the display stops asking until it runs again. */
|
|
106
149
|
readonly muted: undefined;
|
|
150
|
+
/**
|
|
151
|
+
* What the frame reported about annotations (`annotate`, P053), checked and taken only while
|
|
152
|
+
* the host asked for it: a selection, its clearing, a cancelled pick, where marks are. The
|
|
153
|
+
* content's word: UI hints, never permissions.
|
|
154
|
+
*/
|
|
155
|
+
readonly annotation: AnnotationReport;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Annotations on a display whose config declares `annotate` (P053). The host's state is kept and
|
|
159
|
+
* given to every frame the display runs (a reload, a navigation recovered from).
|
|
160
|
+
*/
|
|
161
|
+
export interface AnnotationControls {
|
|
162
|
+
/** Lets the person point at elements, select text, or both (default both). */
|
|
163
|
+
start(mode?: AnnotateMode): void;
|
|
164
|
+
stop(): void;
|
|
165
|
+
/** Marks these anchors (replacing the marks before); the frame reports where they are. */
|
|
166
|
+
show(marks: readonly AnnotationMark[]): void;
|
|
167
|
+
/** Brings a mark into view in the frame and makes it the active one. */
|
|
168
|
+
focus(id: string): void;
|
|
169
|
+
/** Removes every mark. */
|
|
170
|
+
clear(): void;
|
|
107
171
|
}
|
|
108
172
|
export interface DisplayControls {
|
|
109
173
|
readonly displayId: string;
|
|
@@ -115,6 +179,8 @@ export interface DisplayControls {
|
|
|
115
179
|
/** The host's theme values changed (same scheme): read them now and re-theme the frame. */
|
|
116
180
|
refreshTheme(): void;
|
|
117
181
|
on<E extends keyof DisplayEvents>(event: E, listener: (detail: DisplayEvents[E]) => void): () => void;
|
|
182
|
+
/** Annotations (`annotate`): throws on use where the config doesn't declare it. */
|
|
183
|
+
readonly annotations: AnnotationControls;
|
|
118
184
|
/** Runs the content again from the start, in a new frame. */
|
|
119
185
|
reload(): void;
|
|
120
186
|
/** Tears the view down and removes the frame. */
|
package/dist/index.d.ts
CHANGED
|
@@ -2,15 +2,15 @@
|
|
|
2
2
|
* `@botiverse/porthole`: the framework-agnostic core. One config (a preset's) compiles into the
|
|
3
3
|
* frame's policies, the shell, and the agent manual; the host mounts displays under it.
|
|
4
4
|
*/
|
|
5
|
-
export { CAPABILITIES, CAPABILITY_NAMES, capabilitySpec, isCapabilityName, type CapabilityArgs, type CapabilityName, type CapabilityResults, type CapabilitySpec, type Gate, type HandledCapability, } from "./config/capabilities.ts";
|
|
6
|
-
export type { CapabilityConfig, CapabilityMap, ColorScheme, DisplayMode, EgressConfig, EmbedderConfig, GuideConfig, IsolationKind, LayoutConfig, Limits, PortholeConfig, ThemeConfig, ThemeValue, } from "./config/schema.ts";
|
|
5
|
+
export { CAPABILITIES, CAPABILITY_NAMES, MAX_COPY_CHARS, capabilitySpec, isCapabilityName, type CapabilityArgs, type CapabilityName, type CapabilityResults, type CapabilitySpec, type Gate, type HandledCapability, } from "./config/capabilities.ts";
|
|
6
|
+
export type { CapabilityConfig, CapabilityMap, ColorScheme, DisplayMode, EgressConfig, EmbedderConfig, GuideConfig, IsolationKind, LayoutConfig, Limits, PortholeConfig, StorageConfig, ThemeConfig, ThemeValue, } from "./config/schema.ts";
|
|
7
7
|
export { BUILT_IN_TOPICS, ConfigError, isOriginSource, originSourceProblem, validateConfig, } from "./config/validate.ts";
|
|
8
8
|
export { URL_PERMISSIONS, URL_SANDBOX_EXTRAS, type UrlPermission, type UrlSandboxExtra, } from "./config/schema.ts";
|
|
9
9
|
export { hostOriginProblem, isHostOrigin } from "./isolation/served.ts";
|
|
10
10
|
export { FRAME_STATUS_METHOD } from "./isolation/types.ts";
|
|
11
11
|
export { LIBRARY_CDNS, presets, type PresetName, type PresetOptions, type UrlPresetOptions, } from "./presets/presets.ts";
|
|
12
12
|
export { compile, type CompiledConfig } from "./compile/compile.ts";
|
|
13
|
-
export { allEgressOrigins, declaredCapabilities, effectiveGates, egressOrigins, embedderCsp, type CspDirective, type EgressKind, } from "./compile/policy.ts";
|
|
13
|
+
export { allEgressOrigins, declaredCapabilities, effectiveGates, egressOrigins, embedderCsp, embedderFrameSourcesOf, type CspDirective, type EgressKind, } from "./compile/policy.ts";
|
|
14
14
|
export { shellPlan, type ShellPlan } from "./shell/plan.ts";
|
|
15
15
|
export type { ShellDocumentOptions } from "./shell/document.ts";
|
|
16
16
|
export { generateGuide, generateToolDescription, type BuiltInTopic, type GuideTopic, } from "./guide/guide.ts";
|
|
@@ -20,7 +20,10 @@ export type { FrameLocation, FrameSource, InstanceLocator, InstanceRef, ServedDo
|
|
|
20
20
|
export type { PlatformHost, Surface, SurfaceSpec } from "./platform/types.ts";
|
|
21
21
|
export type { Transport } from "./transport/types.ts";
|
|
22
22
|
export type { AdapterConnection, AdapterEvent, AdapterSession, ProtocolAdapter, ShellConnection, } from "./protocol/adapter.ts";
|
|
23
|
-
export { MCP_APPS_METHODS, MCP_APPS_PROTOCOL_VERSION, MCP_APPS_STYLE_VARIABLES, PORTHOLE_STYLES, type HostContext, } from "./protocol/mcp-apps.ts";
|
|
23
|
+
export { COPY_TEXT_METHOD, MCP_APPS_METHODS, MCP_APPS_PROTOCOL_VERSION, MCP_APPS_STYLE_VARIABLES, PORTHOLE_STYLES, type HostContext, } from "./protocol/mcp-apps.ts";
|
|
24
24
|
export type { CallContext, CallOutcome, CapabilityHandler, CapabilityHandlers, Confirm, ConfirmAnswer, ConfirmRequest, Proceed, HostFunctionBridge, Refusal, } from "./bridge/types.ts";
|
|
25
25
|
export { createHost } from "./host/create-host.ts";
|
|
26
|
-
export
|
|
26
|
+
export { writeClipboard } from "./host/clipboard.ts";
|
|
27
|
+
export type { AnnotationControls, DisplayControls, DisplayEvents, FrameBlock, FrameOutcome, DisplayHandle, HostOptions, MountOptions, PortholeHost, } from "./host/types.ts";
|
|
28
|
+
export { ANNOTATE_METHODS, ANNOTATION_LIMITS, type AnchorResult, type AnchorState, type AnnotateMode, type AnnotationAnchor, type AnnotationMark, type AnnotationRect, type AnnotationReport, type AnnotationSelector, type CssSelector, type TextPositionSelector, type TextQuoteSelector, } from "./annotate/types.ts";
|
|
29
|
+
export { parseAnchor, parseRect, parseReport } from "./annotate/validate.ts";
|
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* frame's policies, the shell, and the agent manual; the host mounts displays under it.
|
|
4
4
|
*/
|
|
5
5
|
// The config and its vocabulary.
|
|
6
|
-
export { CAPABILITIES, CAPABILITY_NAMES, capabilitySpec, isCapabilityName, } from "./config/capabilities.js";
|
|
6
|
+
export { CAPABILITIES, CAPABILITY_NAMES, MAX_COPY_CHARS, capabilitySpec, isCapabilityName, } from "./config/capabilities.js";
|
|
7
7
|
export { BUILT_IN_TOPICS, ConfigError, isOriginSource, originSourceProblem, validateConfig, } from "./config/validate.js";
|
|
8
8
|
export { URL_PERMISSIONS, URL_SANDBOX_EXTRAS, } from "./config/schema.js";
|
|
9
9
|
export { hostOriginProblem, isHostOrigin } from "./isolation/served.js";
|
|
@@ -12,11 +12,15 @@ export { FRAME_STATUS_METHOD } from "./isolation/types.js";
|
|
|
12
12
|
export { LIBRARY_CDNS, presets, } from "./presets/presets.js";
|
|
13
13
|
// The compiler.
|
|
14
14
|
export { compile } from "./compile/compile.js";
|
|
15
|
-
export { allEgressOrigins, declaredCapabilities, effectiveGates, egressOrigins, embedderCsp, } from "./compile/policy.js";
|
|
15
|
+
export { allEgressOrigins, declaredCapabilities, effectiveGates, egressOrigins, embedderCsp, embedderFrameSourcesOf, } from "./compile/policy.js";
|
|
16
16
|
export { shellPlan } from "./shell/plan.js";
|
|
17
17
|
export { generateGuide, generateToolDescription, } from "./guide/guide.js";
|
|
18
18
|
export { GUIDE_EXAMPLES } from "./guide/examples.js";
|
|
19
19
|
// Theme.
|
|
20
20
|
export { CHART_PALETTE, DEFAULT_THEME, DEFAULT_THEME_EVENT, DEFAULT_THEME_VARIABLES, resolveTheme, themeDefault, } from "./theme/variables.js";
|
|
21
|
-
export { MCP_APPS_METHODS, MCP_APPS_PROTOCOL_VERSION, MCP_APPS_STYLE_VARIABLES, PORTHOLE_STYLES, } from "./protocol/mcp-apps.js";
|
|
21
|
+
export { COPY_TEXT_METHOD, MCP_APPS_METHODS, MCP_APPS_PROTOCOL_VERSION, MCP_APPS_STYLE_VARIABLES, PORTHOLE_STYLES, } from "./protocol/mcp-apps.js";
|
|
22
22
|
export { createHost } from "./host/create-host.js";
|
|
23
|
+
export { writeClipboard } from "./host/clipboard.js";
|
|
24
|
+
// Annotations (P053): anchors, what a frame reports, and the host's checks on both.
|
|
25
|
+
export { ANNOTATE_METHODS, ANNOTATION_LIMITS, } from "./annotate/types.js";
|
|
26
|
+
export { parseAnchor, parseRect, parseReport } from "./annotate/validate.js";
|
package/dist/mcp/adapter.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { egressOrigins } from "../compile/policy.js";
|
|
2
2
|
import { JSON_RPC_ERRORS, parseJsonRpc, } from "../protocol/jsonrpc.js";
|
|
3
3
|
import { MCP_APPS_METHODS, MCP_APPS_PROTOCOL_VERSION, } from "../protocol/mcp-apps.js";
|
|
4
|
-
import { MCP_APPS_STYLE_VARIABLES } from "../protocol/mcp-apps.js";
|
|
4
|
+
import { COPY_TEXT_METHOD, MCP_APPS_STYLE_VARIABLES } from "../protocol/mcp-apps.js";
|
|
5
5
|
const isRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
6
6
|
/** Levels of `notifications/message` that are errors: they go to the host as `reportError`. */
|
|
7
7
|
const ERROR_LEVELS = new Set(["error", "critical", "alert", "emergency"]);
|
|
@@ -183,6 +183,15 @@ function connect(session, options) {
|
|
|
183
183
|
: { structuredContent: params.structuredContent }),
|
|
184
184
|
}, () => ({}));
|
|
185
185
|
return;
|
|
186
|
+
case COPY_TEXT_METHOD:
|
|
187
|
+
// Porthole's own request (the standard has no clipboard request): the host copies, and
|
|
188
|
+
// shows the person what (P052).
|
|
189
|
+
if (typeof params.text !== "string") {
|
|
190
|
+
invalid("needs text");
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
await call(request, "copyText", { text: params.text }, done);
|
|
194
|
+
return;
|
|
186
195
|
case MCP_APPS_METHODS.requestDisplayMode: {
|
|
187
196
|
if (typeof params.mode !== "string" || !DISPLAY_MODES.has(params.mode)) {
|
|
188
197
|
invalid("needs a display mode");
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { CapabilityName } from "../config/capabilities.ts";
|
|
2
|
-
import type { CapabilityConfig, EgressConfig, EmbedderConfig, GuideConfig, IsolationKind, LayoutConfig, Limits, PortholeConfig, ThemeValue, UrlPermission, UrlSandboxExtra } from "../config/schema.ts";
|
|
2
|
+
import type { CapabilityConfig, EgressConfig, EmbedderConfig, GuideConfig, IsolationKind, LayoutConfig, Limits, PortholeConfig, StorageConfig, ThemeValue, UrlPermission, UrlSandboxExtra } from "../config/schema.ts";
|
|
3
3
|
/**
|
|
4
4
|
* The preset policy layer (P001): named configs with safe defaults. Every preset starts with
|
|
5
5
|
* egress closed and only the capabilities that carry nothing out of the frame (its size, its
|
|
@@ -30,6 +30,8 @@ export interface PresetOptions {
|
|
|
30
30
|
readonly isolation?: Exclude<IsolationKind, "external">;
|
|
31
31
|
readonly contentOrigins?: readonly string[];
|
|
32
32
|
readonly embedder?: EmbedderConfig;
|
|
33
|
+
/** Domain isolation: what displays sharing an origin share (`StorageConfig`), for the manual. */
|
|
34
|
+
readonly storage?: StorageConfig;
|
|
33
35
|
}
|
|
34
36
|
/** The url preset's options: where the app is served, and the embedding page's extra frames. */
|
|
35
37
|
export interface UrlPresetOptions {
|
package/dist/presets/presets.js
CHANGED
|
@@ -102,6 +102,7 @@ function build(mode, defaults, options) {
|
|
|
102
102
|
guide: options.guide ?? {},
|
|
103
103
|
contentOrigins: options.contentOrigins ?? [],
|
|
104
104
|
embedder: options.embedder ?? {},
|
|
105
|
+
...(options.storage === undefined ? {} : { storage: options.storage }),
|
|
105
106
|
});
|
|
106
107
|
}
|
|
107
108
|
export const presets = {
|
|
@@ -47,6 +47,11 @@ export declare const MCP_APPS_STYLE_VARIABLES: readonly string[];
|
|
|
47
47
|
* that doesn't know the field ignores it (the host context is open to extra fields).
|
|
48
48
|
*/
|
|
49
49
|
export declare const PORTHOLE_STYLES = "porthole/styles";
|
|
50
|
+
/**
|
|
51
|
+
* Porthole's own request for copying text (`copyText`, P052): `{ text }`, answered `{}` when the
|
|
52
|
+
* host copied it and `{ isError: true }` when it didn't. The standard has no clipboard request.
|
|
53
|
+
*/
|
|
54
|
+
export declare const COPY_TEXT_METHOD = "porthole/copy-text";
|
|
50
55
|
/** The logger name the shell's error reports (`notifications/message`, level `error`) carry. */
|
|
51
56
|
export declare const SHELL_LOGGER = "porthole";
|
|
52
57
|
/**
|
|
@@ -95,5 +95,10 @@ export const MCP_APPS_STYLE_VARIABLES = [
|
|
|
95
95
|
* that doesn't know the field ignores it (the host context is open to extra fields).
|
|
96
96
|
*/
|
|
97
97
|
export const PORTHOLE_STYLES = "porthole/styles";
|
|
98
|
+
/**
|
|
99
|
+
* Porthole's own request for copying text (`copyText`, P052): `{ text }`, answered `{}` when the
|
|
100
|
+
* host copied it and `{ isError: true }` when it didn't. The standard has no clipboard request.
|
|
101
|
+
*/
|
|
102
|
+
export const COPY_TEXT_METHOD = "porthole/copy-text";
|
|
98
103
|
/** The logger name the shell's error reports (`notifications/message`, level `error`) carry. */
|
|
99
104
|
export const SHELL_LOGGER = "porthole";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { JSON_RPC_ERRORS, parseJsonRpc, } from "./jsonrpc.js";
|
|
2
|
-
import { MCP_APPS_METHODS as M, MCP_APPS_PROTOCOL_VERSION } from "./mcp-apps.js";
|
|
2
|
+
import { COPY_TEXT_METHOD, MCP_APPS_METHODS as M, MCP_APPS_PROTOCOL_VERSION, } from "./mcp-apps.js";
|
|
3
3
|
/**
|
|
4
4
|
* The core's adapter: the host side of the MCP Apps subset the shell speaks, which content using
|
|
5
5
|
* the official App SDK speaks too. Every message is checked before use; every power goes through
|
|
@@ -28,6 +28,7 @@ const REQUESTS = {
|
|
|
28
28
|
}),
|
|
29
29
|
},
|
|
30
30
|
[M.openLink]: { capability: "openLink", args: (params) => ({ url: params.url }) },
|
|
31
|
+
[COPY_TEXT_METHOD]: { capability: "copyText", args: (params) => ({ text: params.text }) },
|
|
31
32
|
[M.downloadFile]: {
|
|
32
33
|
capability: "downloadFile",
|
|
33
34
|
args: (params) => ({ contents: params.contents }),
|
|
@@ -44,7 +45,10 @@ const REQUESTS = {
|
|
|
44
45
|
};
|
|
45
46
|
/** What a capability's outcome looks like to the view. */
|
|
46
47
|
function answer(capability, outcome) {
|
|
47
|
-
const flagged = capability === "sendPrompt" ||
|
|
48
|
+
const flagged = capability === "sendPrompt" ||
|
|
49
|
+
capability === "openLink" ||
|
|
50
|
+
capability === "downloadFile" ||
|
|
51
|
+
capability === "copyText";
|
|
48
52
|
if (!outcome.ok) {
|
|
49
53
|
return flagged
|
|
50
54
|
? { result: { isError: true } }
|
package/dist/react/dialog.js
CHANGED
|
@@ -47,6 +47,8 @@ function words(request) {
|
|
|
47
47
|
return { heading: "Share this with the agent?", accept: "Share", detail: json(request.args) };
|
|
48
48
|
case "readData":
|
|
49
49
|
return { heading: "Let it read this?", accept: "Allow", detail: request.args.uri };
|
|
50
|
+
case "copyText":
|
|
51
|
+
return { heading: "Copy this text?", accept: "Copy", detail: request.args.text };
|
|
50
52
|
default:
|
|
51
53
|
return { heading: "Allow this?", accept: "Allow", detail: json(request.args) };
|
|
52
54
|
}
|
package/dist/react/frame.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { type CSSProperties, type ReactElement, type ReactNode } from "react";
|
|
2
|
+
import type { AnnotateMode, AnnotationMark, AnnotationReport } from "../annotate/types.ts";
|
|
2
3
|
import type { CapabilityHandlers, Confirm, Refusal } from "../bridge/types.ts";
|
|
3
4
|
import type { CapabilityName } from "../config/capabilities.ts";
|
|
4
5
|
import type { ColorScheme, PortholeConfig } from "../config/schema.ts";
|
|
5
|
-
import type { FrameOutcome } from "../host/types.ts";
|
|
6
|
+
import type { FrameOutcome, HostErrorKind } from "../host/types.ts";
|
|
6
7
|
import type { InstanceLocator, InstanceRef } from "../isolation/types.ts";
|
|
7
8
|
/** The height a display holds before it has reported its own. */
|
|
8
9
|
export declare const PLACEHOLDER_HEIGHT_PX = 96;
|
|
@@ -21,6 +22,13 @@ export interface PortholeProviderProps {
|
|
|
21
22
|
readonly locator?: InstanceLocator;
|
|
22
23
|
/** A class for the default dialog, to restyle it. */
|
|
23
24
|
readonly dialogClassName?: string;
|
|
25
|
+
/** A class for the default copy toast, to restyle it. */
|
|
26
|
+
readonly toastClassName?: string;
|
|
27
|
+
/**
|
|
28
|
+
* The page's CSP nonce, where its `script-src` is nonce-based: opaque frames inherit the page's
|
|
29
|
+
* policy, so their shell carries it (P054).
|
|
30
|
+
*/
|
|
31
|
+
readonly nonce?: string;
|
|
24
32
|
readonly children?: ReactNode;
|
|
25
33
|
}
|
|
26
34
|
export declare function PortholeProvider(props: PortholeProviderProps): ReactElement;
|
|
@@ -57,12 +65,31 @@ export interface PortholeFrameProps {
|
|
|
57
65
|
readonly generation?: number;
|
|
58
66
|
/** Run at once, without waiting to near the screen (it still counts toward the page's most). */
|
|
59
67
|
readonly eager?: boolean;
|
|
68
|
+
/**
|
|
69
|
+
* Fill the box (give it, or its parent, a height) and let the content scroll inside, instead of
|
|
70
|
+
* growing with the content: an artifact viewer's page. A url-mode app always fills.
|
|
71
|
+
*/
|
|
72
|
+
readonly fill?: boolean;
|
|
60
73
|
readonly onInitialized?: () => void;
|
|
61
74
|
readonly onResize?: (size: {
|
|
62
75
|
readonly height: number;
|
|
63
76
|
readonly width?: number;
|
|
64
77
|
}) => void;
|
|
65
|
-
|
|
78
|
+
/**
|
|
79
|
+
* What the content did wrong (what it threw, what the sandbox blocked, that it kept navigating
|
|
80
|
+
* away): the agent's to hear and fix.
|
|
81
|
+
*/
|
|
82
|
+
readonly onContentError?: (message: string) => void;
|
|
83
|
+
/**
|
|
84
|
+
* What failed on the host's side: its locator (`locate`), a display that couldn't be made
|
|
85
|
+
* (`mount`), an annotation call refused (`annotations`). Never the agent's.
|
|
86
|
+
*/
|
|
87
|
+
readonly onHostError?: (error: {
|
|
88
|
+
readonly message: string;
|
|
89
|
+
readonly kind: HostErrorKind;
|
|
90
|
+
}) => void;
|
|
91
|
+
/** Both, with their source (as before 0.1.4). Prefer `onContentError` and `onHostError`. */
|
|
92
|
+
readonly onError?: (message: string, source: "content" | "host") => void;
|
|
66
93
|
/** The content navigated its frame away; it was started again (or stopped, after a few). */
|
|
67
94
|
readonly onNavigated?: () => void;
|
|
68
95
|
readonly onRefused?: (capability: CapabilityName, refusal: Refusal) => void;
|
|
@@ -83,6 +110,27 @@ export interface PortholeFrameProps {
|
|
|
83
110
|
* restyle): the running frame reads them again.
|
|
84
111
|
*/
|
|
85
112
|
readonly themeKey?: string | number;
|
|
113
|
+
/**
|
|
114
|
+
* Annotations, where the config declares `annotate` (P053): what the person may point at now
|
|
115
|
+
* (elements, text, or both), or null to stop. A picked element ends element picking: set it
|
|
116
|
+
* again to pick again.
|
|
117
|
+
*/
|
|
118
|
+
readonly annotating?: AnnotateMode | null;
|
|
119
|
+
/** The anchors to mark in the frame (the host's comments). Keep it stable (memoized). */
|
|
120
|
+
readonly annotations?: readonly AnnotationMark[];
|
|
121
|
+
/**
|
|
122
|
+
* The mark to bring into view and make the active one; each new id focuses it. To focus the
|
|
123
|
+
* same mark again (a second click on its comment), give `{ id, key }` with a new key.
|
|
124
|
+
*/
|
|
125
|
+
readonly focusedAnnotation?: string | {
|
|
126
|
+
readonly id: string;
|
|
127
|
+
readonly key: string | number;
|
|
128
|
+
} | null;
|
|
129
|
+
/**
|
|
130
|
+
* What the frame reported: a selection (an anchor and where it is), its clearing, a cancelled
|
|
131
|
+
* pick, where marks are. The content's word: UI hints, never permissions.
|
|
132
|
+
*/
|
|
133
|
+
readonly onAnnotation?: (report: AnnotationReport) => void;
|
|
86
134
|
/** Draws the placeholder instead of the default one. */
|
|
87
135
|
readonly renderPlaceholder?: (props: PlaceholderProps) => ReactNode;
|
|
88
136
|
readonly className?: string;
|