@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.
Files changed (84) hide show
  1. package/dist/annotate/types.d.ts +120 -0
  2. package/dist/annotate/types.js +42 -0
  3. package/dist/annotate/validate.d.ts +11 -0
  4. package/dist/annotate/validate.js +156 -0
  5. package/dist/bridge/bridge.d.ts +4 -0
  6. package/dist/bridge/bridge.js +12 -1
  7. package/dist/compile/policy.d.ts +10 -3
  8. package/dist/compile/policy.js +16 -5
  9. package/dist/config/capabilities.d.ts +37 -4
  10. package/dist/config/capabilities.js +20 -0
  11. package/dist/config/schema.d.ts +11 -0
  12. package/dist/config/validate.js +12 -0
  13. package/dist/conformance/clipboard.d.ts +2 -0
  14. package/dist/conformance/clipboard.js +167 -0
  15. package/dist/conformance/kit.d.ts +1 -1
  16. package/dist/conformance/probes.js +2 -0
  17. package/dist/conformance/served.d.ts +8 -1
  18. package/dist/conformance/served.js +12 -8
  19. package/dist/guide/guide.js +11 -5
  20. package/dist/host/clipboard.d.ts +11 -0
  21. package/dist/host/clipboard.js +43 -0
  22. package/dist/host/create-host.js +165 -7
  23. package/dist/host/types.d.ts +68 -2
  24. package/dist/index.d.ts +8 -5
  25. package/dist/index.js +7 -3
  26. package/dist/mcp/adapter.js +10 -1
  27. package/dist/presets/presets.d.ts +3 -1
  28. package/dist/presets/presets.js +1 -0
  29. package/dist/protocol/mcp-apps.d.ts +5 -0
  30. package/dist/protocol/mcp-apps.js +5 -0
  31. package/dist/protocol/shell-adapter.js +6 -2
  32. package/dist/react/dialog.js +2 -0
  33. package/dist/react/frame.d.ts +50 -2
  34. package/dist/react/frame.js +153 -11
  35. package/dist/react/index.d.ts +2 -1
  36. package/dist/react/index.js +2 -1
  37. package/dist/react/toast.d.ts +33 -0
  38. package/dist/react/toast.js +42 -0
  39. package/dist/server/naming.d.ts +6 -0
  40. package/dist/server/naming.js +38 -0
  41. package/dist/server/pages.js +2 -0
  42. package/dist/server/server.js +49 -17
  43. package/dist/server/types.d.ts +6 -3
  44. package/dist/shell/annotate.d.ts +20 -0
  45. package/dist/shell/annotate.js +700 -0
  46. package/dist/shell/document.d.ts +6 -0
  47. package/dist/shell/document.js +12 -3
  48. package/dist/shell/plan.d.ts +2 -0
  49. package/dist/shell/plan.js +2 -1
  50. package/dist/shell/runtime.d.ts +4 -1
  51. package/dist/shell/runtime.js +71 -3
  52. package/package.json +1 -1
  53. package/src/annotate/types.ts +126 -0
  54. package/src/annotate/validate.ts +186 -0
  55. package/src/bridge/bridge.ts +13 -0
  56. package/src/compile/policy.ts +17 -5
  57. package/src/config/capabilities.ts +33 -3
  58. package/src/config/schema.ts +12 -0
  59. package/src/config/validate.ts +15 -0
  60. package/src/conformance/clipboard.ts +184 -0
  61. package/src/conformance/kit.ts +1 -1
  62. package/src/conformance/probes.ts +2 -0
  63. package/src/conformance/served.ts +24 -9
  64. package/src/guide/guide.ts +22 -5
  65. package/src/host/clipboard.ts +41 -0
  66. package/src/host/create-host.ts +181 -7
  67. package/src/host/types.ts +64 -2
  68. package/src/index.ts +24 -0
  69. package/src/mcp/adapter.ts +10 -1
  70. package/src/presets/presets.ts +4 -0
  71. package/src/protocol/mcp-apps.ts +6 -0
  72. package/src/protocol/shell-adapter.ts +11 -2
  73. package/src/react/dialog.tsx +2 -0
  74. package/src/react/frame.tsx +231 -13
  75. package/src/react/index.ts +9 -1
  76. package/src/react/toast.tsx +96 -0
  77. package/src/server/naming.ts +41 -0
  78. package/src/server/pages.ts +2 -0
  79. package/src/server/server.ts +61 -20
  80. package/src/server/types.ts +6 -3
  81. package/src/shell/annotate.ts +700 -0
  82. package/src/shell/document.ts +21 -3
  83. package/src/shell/plan.ts +4 -1
  84. package/src/shell/runtime.ts +71 -3
@@ -1,4 +1,12 @@
1
+ import {
2
+ ANNOTATE_METHODS,
3
+ type AnnotateMode,
4
+ type AnnotationMark,
5
+ type AnnotationReport,
6
+ } from "../annotate/types.ts";
7
+ import { annotateMode, checkMarks, parseReport } from "../annotate/validate.ts";
1
8
  import { createBridge, type BridgeEvent } from "../bridge/bridge.ts";
9
+ import type { HostFunctionBridge } from "../bridge/types.ts";
2
10
  import { compile } from "../compile/compile.ts";
3
11
  import { declaredCapabilities } from "../compile/policy.ts";
4
12
  import { capabilitySpec } from "../config/capabilities.ts";
@@ -17,6 +25,8 @@ import {
17
25
  import { shellAdapter } from "../protocol/shell-adapter.ts";
18
26
  import { resolveTheme } from "../theme/variables.ts";
19
27
  import type {
28
+ AnnotationControls,
29
+ HostErrorKind,
20
30
  DisplayEvents,
21
31
  DisplayHandle,
22
32
  HostOptions,
@@ -31,6 +41,17 @@ const DEFAULT_LOAD_TIMEOUT_MS = 15_000;
31
41
  const MAX_RESTARTS = 3;
32
42
 
33
43
  const BASE_KEYS = new Set(["hostContextChanged", "teardown", "close"]);
44
+
45
+ /** The frame's annotation reports, by method. */
46
+ const REPORTS: Readonly<Record<string, AnnotationReport["type"]>> = {
47
+ [ANNOTATE_METHODS.selected]: "selected",
48
+ [ANNOTATE_METHODS.unselected]: "unselected",
49
+ [ANNOTATE_METHODS.cancelled]: "cancelled",
50
+ [ANNOTATE_METHODS.anchored]: "anchored",
51
+ };
52
+
53
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
54
+ value !== null && typeof value === "object" && !Array.isArray(value);
34
55
  const STANDARD = new Set(MCP_APPS_STYLE_VARIABLES);
35
56
 
36
57
  /**
@@ -76,6 +97,16 @@ export function createHost<C extends AdapterConnection = ShellConnection>(
76
97
  (listener as (detail: DisplayEvents[E]) => void)(detail);
77
98
  }
78
99
  };
100
+ // What the content did wrong (the agent's to hear and fix), and what failed on the host's
101
+ // side (its own to handle), each on its own event and on `error` with its source.
102
+ const contentError = (message: string) => {
103
+ emit("contentError", { message });
104
+ emit("error", { message, source: "content" });
105
+ };
106
+ const hostError = (message: string, kind: HostErrorKind) => {
107
+ emit("hostError", { message, kind });
108
+ emit("error", { message, source: "host" });
109
+ };
79
110
 
80
111
  let scheme: ColorScheme = mount.scheme;
81
112
  let height = mount.initialHeightPx ?? 0;
@@ -90,6 +121,129 @@ export function createHost<C extends AdapterConnection = ShellConnection>(
90
121
  const view = container.ownerDocument.defaultView;
91
122
  // The adapter's own operations, the last call of each, replayed into every new frame.
92
123
  const replay = new Map<string, unknown[]>();
124
+ // Annotations: the host's state, given to each frame once it finished its handshake.
125
+ const annotates = config.capabilities.annotate !== undefined;
126
+ let annotating: Required<AnnotateMode> | null = null;
127
+ let marks: AnnotationMark[] = [];
128
+ let activeMark: string | null = null;
129
+ let handshaken = false;
130
+ const toFrame = (method: string, params: Record<string, unknown>) => {
131
+ if (handshaken && surface !== null) {
132
+ surface.transport.send({ jsonrpc: "2.0", method, params });
133
+ }
134
+ };
135
+ const giveAnnotations = () => {
136
+ if (marks.length > 0) {
137
+ toFrame(ANNOTATE_METHODS.show, {
138
+ marks,
139
+ ...(activeMark === null ? {} : { active: activeMark }),
140
+ });
141
+ }
142
+ if (annotating !== null) {
143
+ toFrame(ANNOTATE_METHODS.start, { ...annotating });
144
+ }
145
+ };
146
+ /**
147
+ * A report from the frame: checked, taken only while the host asked for it (a selection
148
+ * only in select mode, results only for its own marks), then through the bridge's gates.
149
+ */
150
+ const onAnnotation = (raw: unknown, bridge: HostFunctionBridge) => {
151
+ if (
152
+ !isRecord(raw) ||
153
+ raw.jsonrpc !== "2.0" ||
154
+ "id" in raw ||
155
+ typeof raw.method !== "string"
156
+ ) {
157
+ return;
158
+ }
159
+ const type = REPORTS[raw.method];
160
+ if (type === undefined) {
161
+ return;
162
+ }
163
+ let report = parseReport({ ...(isRecord(raw.params) ? raw.params : {}), type });
164
+ if (report === null) {
165
+ void bridge.call("annotate", null);
166
+ return;
167
+ }
168
+ switch (report.type) {
169
+ case "selected": {
170
+ const kind = report.anchor.kind;
171
+ if (
172
+ annotating === null ||
173
+ !(kind === "element" ? annotating.elements : annotating.text)
174
+ ) {
175
+ return;
176
+ }
177
+ if (kind === "element") {
178
+ annotating = annotating.text ? { elements: false, text: true } : null;
179
+ }
180
+ break;
181
+ }
182
+ case "unselected":
183
+ if (annotating?.text !== true) {
184
+ return;
185
+ }
186
+ break;
187
+ case "cancelled":
188
+ if (annotating?.elements !== true) {
189
+ return;
190
+ }
191
+ annotating = annotating.text ? { elements: false, text: true } : null;
192
+ break;
193
+ case "anchored": {
194
+ const known = new Set(marks.map((mark) => mark.id));
195
+ const results = report.results.filter((result) => known.has(result.id));
196
+ if (results.length === 0) {
197
+ return;
198
+ }
199
+ report = { type: "anchored", results };
200
+ break;
201
+ }
202
+ }
203
+ void bridge.call("annotate", report);
204
+ };
205
+ const needsAnnotate = (operation: string) => {
206
+ if (!annotates) {
207
+ throw new TypeError(`annotations.${operation}: the config doesn't declare annotate`);
208
+ }
209
+ };
210
+ const annotations: AnnotationControls = {
211
+ start(mode) {
212
+ needsAnnotate("start");
213
+ annotating = annotateMode(mode);
214
+ toFrame(ANNOTATE_METHODS.start, { ...annotating });
215
+ },
216
+ stop() {
217
+ needsAnnotate("stop");
218
+ annotating = null;
219
+ toFrame(ANNOTATE_METHODS.stop, {});
220
+ },
221
+ show(next) {
222
+ needsAnnotate("show");
223
+ marks = checkMarks(next);
224
+ if (activeMark !== null && !marks.some((mark) => mark.id === activeMark)) {
225
+ activeMark = null;
226
+ }
227
+ toFrame(ANNOTATE_METHODS.show, {
228
+ marks,
229
+ ...(activeMark === null ? {} : { active: activeMark }),
230
+ });
231
+ },
232
+ focus(id) {
233
+ needsAnnotate("focus");
234
+ if (!marks.some((mark) => mark.id === id)) {
235
+ throw new TypeError(`annotations.focus: no mark ${JSON.stringify(id)} is shown`);
236
+ }
237
+ activeMark = id;
238
+ toFrame(ANNOTATE_METHODS.focus, { id });
239
+ },
240
+ clear() {
241
+ needsAnnotate("clear");
242
+ marks = [];
243
+ activeMark = null;
244
+ toFrame(ANNOTATE_METHODS.clear, {});
245
+ },
246
+ };
93
247
 
94
248
  const hostContext = (): HostContext => {
95
249
  const width = container.getBoundingClientRect().width;
@@ -134,7 +288,7 @@ export function createHost<C extends AdapterConnection = ShellConnection>(
134
288
  return;
135
289
  }
136
290
  case "error":
137
- emit("error", { message: event.message });
291
+ contentError(event.message);
138
292
  return;
139
293
  case "refused":
140
294
  emit("refused", { capability: event.capability, refusal: event.refusal });
@@ -142,10 +296,14 @@ export function createHost<C extends AdapterConnection = ShellConnection>(
142
296
  case "muted":
143
297
  emit("muted", undefined);
144
298
  return;
299
+ case "annotation":
300
+ emit("annotation", event.report);
301
+ return;
145
302
  }
146
303
  };
147
304
 
148
305
  const stop = () => {
306
+ handshaken = false;
149
307
  abort?.abort();
150
308
  abort = null;
151
309
  outcomeWatch?.stop();
@@ -164,8 +322,9 @@ export function createHost<C extends AdapterConnection = ShellConnection>(
164
322
  allow: compiled.allow,
165
323
  referrerPolicy: compiled.referrerPolicy,
166
324
  colorScheme: scheme,
167
- // An app reports no size: its frame fills the host's container, at least the minimum.
168
- ...(speaks
325
+ // An app reports no size: its frame fills the host's container, at least the minimum;
326
+ // so does any display the host mounts to fill (its content scrolls inside).
327
+ ...(speaks && mount.fill !== true
169
328
  ? { initialHeightPx: height }
170
329
  : { fill: true, initialHeightPx: config.limits.minHeightPx }),
171
330
  });
@@ -197,6 +356,9 @@ export function createHost<C extends AdapterConnection = ShellConnection>(
197
356
  userActivation: () => created.userActivation(),
198
357
  onEvent: onBridge,
199
358
  });
359
+ if (annotates) {
360
+ created.transport.listen((raw) => onAnnotation(raw, bridge));
361
+ }
200
362
  const opened = adapter.connect({
201
363
  display: { id: mount.displayId, title: mount.title },
202
364
  compiled,
@@ -207,6 +369,10 @@ export function createHost<C extends AdapterConnection = ShellConnection>(
207
369
  emit: (event) => {
208
370
  if (event.type === "initialized") {
209
371
  watch.initialized();
372
+ if (!handshaken) {
373
+ handshaken = true;
374
+ giveAnnotations();
375
+ }
210
376
  emit("initialized", undefined);
211
377
  }
212
378
  },
@@ -223,9 +389,7 @@ export function createHost<C extends AdapterConnection = ShellConnection>(
223
389
  run();
224
390
  } else {
225
391
  stop();
226
- emit("error", {
227
- message: "The content kept navigating its frame away; it was stopped.",
228
- });
392
+ contentError("The content kept navigating its frame away; it was stopped.");
229
393
  }
230
394
  });
231
395
  };
@@ -242,6 +406,7 @@ export function createHost<C extends AdapterConnection = ShellConnection>(
242
406
  html: compiled.shellDocument({
243
407
  origins: [container.ownerDocument.defaultView?.location.origin ?? ""],
244
408
  title: mount.title,
409
+ ...nonceOf(options.nonce),
245
410
  ...(mount.document === undefined ? {} : { document: mount.document }),
246
411
  }),
247
412
  });
@@ -264,7 +429,9 @@ export function createHost<C extends AdapterConnection = ShellConnection>(
264
429
  },
265
430
  (error: unknown) => {
266
431
  if (mine === generation && !controller.signal.aborted) {
267
- emit("error", { message: error instanceof Error ? error.message : String(error) });
432
+ // The host's own failure (its backend down, access gone): never the content's.
433
+ hostError(error instanceof Error ? error.message : String(error), "locate");
434
+ emit("outcome", { state: "blocked", reason: "locate" });
268
435
  }
269
436
  },
270
437
  );
@@ -274,6 +441,7 @@ export function createHost<C extends AdapterConnection = ShellConnection>(
274
441
 
275
442
  const controls = {
276
443
  displayId: mount.displayId,
444
+ annotations,
277
445
  setTheme(next: ColorScheme) {
278
446
  scheme = next;
279
447
  surface?.setColorScheme(next);
@@ -333,3 +501,9 @@ export function createHost<C extends AdapterConnection = ShellConnection>(
333
501
  },
334
502
  };
335
503
  }
504
+
505
+ /** The host's nonce option, read now (`HostOptions.nonce`). */
506
+ function nonceOf(nonce: HostOptions["nonce"]): { nonce?: string } {
507
+ const value = typeof nonce === "function" ? nonce() : nonce;
508
+ return value === undefined || value === "" ? {} : { nonce: value };
509
+ }
package/src/host/types.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";
@@ -33,6 +34,13 @@ export interface HostOptions<C extends AdapterConnection = ShellConnection> {
33
34
  readonly platform?: PlatformHost;
34
35
  /** Required for domain and external isolation: where an instance's frame loads from. */
35
36
  readonly locator?: InstanceLocator;
37
+ /**
38
+ * The embedding page's CSP nonce, where its `script-src` is nonce-based (P054): an opaque frame
39
+ * (`srcdoc`) inherits the page's policy, so the shell and the content's scripts carry it. A
40
+ * function is read each time a frame starts (a page whose nonce changes). Served frames inherit
41
+ * nothing and need none.
42
+ */
43
+ readonly nonce?: string | (() => string | undefined);
36
44
  }
37
45
 
38
46
  export interface MountOptions {
@@ -49,8 +57,23 @@ 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
  }
53
67
 
68
+ /** What failed on the host's side (`hostError`). */
69
+ export type HostErrorKind =
70
+ /** The locator rejected: the host's backend didn't give a location. */
71
+ | "locate"
72
+ /** A display couldn't be made (the React binding: a bad config, a missing handler). */
73
+ | "mount"
74
+ /** An annotation call was refused (an invalid mark, an unknown id). */
75
+ | "annotations";
76
+
54
77
  /** What a display reports, by event name. */
55
78
  /** Why a display's frame showed nothing of its content (`FrameOutcome`). */
56
79
  export type FrameBlock =
@@ -64,6 +87,8 @@ export type FrameBlock =
64
87
  | "upstream"
65
88
  /** The host's own check refused it (revoked, no longer a member): `code` says which. */
66
89
  | "denied"
90
+ /** The host's locator failed (its backend down, access gone): see the `hostError` event. */
91
+ | "locate"
67
92
  | "not-found"
68
93
  | "error";
69
94
 
@@ -92,14 +117,49 @@ export interface DisplayEvents {
92
117
  readonly outcome: FrameOutcome;
93
118
  /** The content's size, after the config's limits. */
94
119
  readonly resize: { readonly height: number; readonly width?: number };
95
- /** What the content threw, or what the sandbox blocked (its own words: untrusted). */
96
- readonly error: { readonly message: string };
120
+ /**
121
+ * What the content did wrong: what it threw, what the sandbox blocked (its own words:
122
+ * untrusted), or that it kept navigating its frame away. The agent's to hear and fix.
123
+ */
124
+ readonly contentError: { readonly message: string };
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: { readonly message: string; readonly kind: HostErrorKind };
130
+ /**
131
+ * Both of the above, with their `source`: for a host that listened here before they were
132
+ * split (0.1.3). Prefer `contentError` and `hostError`.
133
+ */
134
+ readonly error: { readonly message: string; readonly source: "content" | "host" };
97
135
  /** The content navigated its frame away; the frame was started again from its content. */
98
136
  readonly navigated: undefined;
99
137
  /** A call didn't run. */
100
138
  readonly refused: { readonly capability: CapabilityName; readonly refusal: Refusal };
101
139
  /** Too many declines: the display stops asking until it runs again. */
102
140
  readonly muted: undefined;
141
+ /**
142
+ * What the frame reported about annotations (`annotate`, P053), checked and taken only while
143
+ * the host asked for it: a selection, its clearing, a cancelled pick, where marks are. The
144
+ * content's word: UI hints, never permissions.
145
+ */
146
+ readonly annotation: AnnotationReport;
147
+ }
148
+
149
+ /**
150
+ * Annotations on a display whose config declares `annotate` (P053). The host's state is kept and
151
+ * given to every frame the display runs (a reload, a navigation recovered from).
152
+ */
153
+ export interface AnnotationControls {
154
+ /** Lets the person point at elements, select text, or both (default both). */
155
+ start(mode?: AnnotateMode): void;
156
+ stop(): void;
157
+ /** Marks these anchors (replacing the marks before); the frame reports where they are. */
158
+ show(marks: readonly AnnotationMark[]): void;
159
+ /** Brings a mark into view in the frame and makes it the active one. */
160
+ focus(id: string): void;
161
+ /** Removes every mark. */
162
+ clear(): void;
103
163
  }
104
164
 
105
165
  export interface DisplayControls {
@@ -115,6 +175,8 @@ export interface DisplayControls {
115
175
  event: E,
116
176
  listener: (detail: DisplayEvents[E]) => void,
117
177
  ): () => void;
178
+ /** Annotations (`annotate`): throws on use where the config doesn't declare it. */
179
+ readonly annotations: AnnotationControls;
118
180
  /** Runs the content again from the start, in a new frame. */
119
181
  reload(): void;
120
182
  /** Tears the view down and removes the frame. */
package/src/index.ts CHANGED
@@ -7,6 +7,7 @@
7
7
  export {
8
8
  CAPABILITIES,
9
9
  CAPABILITY_NAMES,
10
+ MAX_COPY_CHARS,
10
11
  capabilitySpec,
11
12
  isCapabilityName,
12
13
  type CapabilityArgs,
@@ -28,6 +29,7 @@ export type {
28
29
  LayoutConfig,
29
30
  Limits,
30
31
  PortholeConfig,
32
+ StorageConfig,
31
33
  ThemeConfig,
32
34
  ThemeValue,
33
35
  } from "./config/schema.ts";
@@ -64,6 +66,7 @@ export {
64
66
  effectiveGates,
65
67
  egressOrigins,
66
68
  embedderCsp,
69
+ embedderFrameSourcesOf,
67
70
  type CspDirective,
68
71
  type EgressKind,
69
72
  } from "./compile/policy.ts";
@@ -105,6 +108,7 @@ export type {
105
108
  ShellConnection,
106
109
  } from "./protocol/adapter.ts";
107
110
  export {
111
+ COPY_TEXT_METHOD,
108
112
  MCP_APPS_METHODS,
109
113
  MCP_APPS_PROTOCOL_VERSION,
110
114
  MCP_APPS_STYLE_VARIABLES,
@@ -124,7 +128,9 @@ export type {
124
128
  Refusal,
125
129
  } from "./bridge/types.ts";
126
130
  export { createHost } from "./host/create-host.ts";
131
+ export { writeClipboard } from "./host/clipboard.ts";
127
132
  export type {
133
+ AnnotationControls,
128
134
  DisplayControls,
129
135
  DisplayEvents,
130
136
  FrameBlock,
@@ -134,3 +140,21 @@ export type {
134
140
  MountOptions,
135
141
  PortholeHost,
136
142
  } from "./host/types.ts";
143
+
144
+ // Annotations (P053): anchors, what a frame reports, and the host's checks on both.
145
+ export {
146
+ ANNOTATE_METHODS,
147
+ ANNOTATION_LIMITS,
148
+ type AnchorResult,
149
+ type AnchorState,
150
+ type AnnotateMode,
151
+ type AnnotationAnchor,
152
+ type AnnotationMark,
153
+ type AnnotationRect,
154
+ type AnnotationReport,
155
+ type AnnotationSelector,
156
+ type CssSelector,
157
+ type TextPositionSelector,
158
+ type TextQuoteSelector,
159
+ } from "./annotate/types.ts";
160
+ export { parseAnchor, parseRect, parseReport } from "./annotate/validate.ts";
@@ -14,7 +14,7 @@ import {
14
14
  MCP_APPS_PROTOCOL_VERSION,
15
15
  type HostContext,
16
16
  } from "../protocol/mcp-apps.ts";
17
- import { MCP_APPS_STYLE_VARIABLES } from "../protocol/mcp-apps.ts";
17
+ import { COPY_TEXT_METHOD, MCP_APPS_STYLE_VARIABLES } from "../protocol/mcp-apps.ts";
18
18
 
19
19
  /**
20
20
  * The MCP Apps adapter (P004, P007): the host side of the protocol for full MCP Apps views, written
@@ -283,6 +283,15 @@ function connect(session: AdapterSession, options: McpAppsAdapterOptions): McpAp
283
283
  () => ({}),
284
284
  );
285
285
  return;
286
+ case COPY_TEXT_METHOD:
287
+ // Porthole's own request (the standard has no clipboard request): the host copies, and
288
+ // shows the person what (P052).
289
+ if (typeof params.text !== "string") {
290
+ invalid("needs text");
291
+ return;
292
+ }
293
+ await call(request, "copyText", { text: params.text }, done);
294
+ return;
286
295
  case MCP_APPS_METHODS.requestDisplayMode: {
287
296
  if (typeof params.mode !== "string" || !DISPLAY_MODES.has(params.mode)) {
288
297
  invalid("needs a display mode");
@@ -9,6 +9,7 @@ import type {
9
9
  LayoutConfig,
10
10
  Limits,
11
11
  PortholeConfig,
12
+ StorageConfig,
12
13
  ThemeValue,
13
14
  UrlPermission,
14
15
  UrlSandboxExtra,
@@ -45,6 +46,8 @@ export interface PresetOptions {
45
46
  readonly isolation?: Exclude<IsolationKind, "external">;
46
47
  readonly contentOrigins?: readonly string[];
47
48
  readonly embedder?: EmbedderConfig;
49
+ /** Domain isolation: what displays sharing an origin share (`StorageConfig`), for the manual. */
50
+ readonly storage?: StorageConfig;
48
51
  }
49
52
 
50
53
  /** The url preset's options: where the app is served, and the embedding page's extra frames. */
@@ -174,6 +177,7 @@ function build(
174
177
  guide: options.guide ?? {},
175
178
  contentOrigins: options.contentOrigins ?? [],
176
179
  embedder: options.embedder ?? {},
180
+ ...(options.storage === undefined ? {} : { storage: options.storage }),
177
181
  });
178
182
  }
179
183
 
@@ -115,6 +115,12 @@ export const MCP_APPS_STYLE_VARIABLES: readonly string[] = [
115
115
  */
116
116
  export const PORTHOLE_STYLES = "porthole/styles";
117
117
 
118
+ /**
119
+ * Porthole's own request for copying text (`copyText`, P052): `{ text }`, answered `{}` when the
120
+ * host copied it and `{ isError: true }` when it didn't. The standard has no clipboard request.
121
+ */
122
+ export const COPY_TEXT_METHOD = "porthole/copy-text";
123
+
118
124
  /** The logger name the shell's error reports (`notifications/message`, level `error`) carry. */
119
125
  export const SHELL_LOGGER = "porthole";
120
126
 
@@ -8,7 +8,12 @@ import {
8
8
  type JsonRpcRequest,
9
9
  type JsonRpcResponse,
10
10
  } from "./jsonrpc.ts";
11
- import { MCP_APPS_METHODS as M, MCP_APPS_PROTOCOL_VERSION, type HostContext } from "./mcp-apps.ts";
11
+ import {
12
+ COPY_TEXT_METHOD,
13
+ MCP_APPS_METHODS as M,
14
+ MCP_APPS_PROTOCOL_VERSION,
15
+ type HostContext,
16
+ } from "./mcp-apps.ts";
12
17
 
13
18
  /**
14
19
  * The core's adapter: the host side of the MCP Apps subset the shell speaks, which content using
@@ -42,6 +47,7 @@ const REQUESTS: Readonly<
42
47
  }),
43
48
  },
44
49
  [M.openLink]: { capability: "openLink", args: (params) => ({ url: params.url }) },
50
+ [COPY_TEXT_METHOD]: { capability: "copyText", args: (params) => ({ text: params.text }) },
45
51
  [M.downloadFile]: {
46
52
  capability: "downloadFile",
47
53
  args: (params) => ({ contents: params.contents }),
@@ -63,7 +69,10 @@ function answer(
63
69
  outcome: CallOutcome<unknown>,
64
70
  ): { result: unknown } | { error: { code: number; message: string } } {
65
71
  const flagged =
66
- capability === "sendPrompt" || capability === "openLink" || capability === "downloadFile";
72
+ capability === "sendPrompt" ||
73
+ capability === "openLink" ||
74
+ capability === "downloadFile" ||
75
+ capability === "copyText";
67
76
  if (!outcome.ok) {
68
77
  return flagged
69
78
  ? { result: { isError: true } }
@@ -66,6 +66,8 @@ function words(request: ConfirmRequest): Words {
66
66
  return { heading: "Share this with the agent?", accept: "Share", detail: json(request.args) };
67
67
  case "readData":
68
68
  return { heading: "Let it read this?", accept: "Allow", detail: request.args.uri };
69
+ case "copyText":
70
+ return { heading: "Copy this text?", accept: "Copy", detail: request.args.text };
69
71
  default:
70
72
  return { heading: "Allow this?", accept: "Allow", detail: json(request.args) };
71
73
  }