@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,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
- emit("error", { message: event.message });
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
- ...(speaks
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
- emit("error", {
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
- emit("error", { message: error instanceof Error ? error.message : String(error) });
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
+ }
@@ -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" | "not-found" | "error";
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
- /** What the content threw, or what the sandbox blocked (its own words: untrusted). */
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 type { DisplayControls, DisplayEvents, FrameBlock, FrameOutcome, DisplayHandle, HostOptions, MountOptions, PortholeHost, } from "./host/types.ts";
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";
@@ -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 {
@@ -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" || capability === "openLink" || capability === "downloadFile";
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 } }
@@ -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
  }
@@ -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
- readonly onError?: (message: string) => void;
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;