@botiverse/porthole 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +217 -0
- package/dist/bridge/bridge.d.ts +39 -0
- package/dist/bridge/bridge.js +184 -0
- package/dist/bridge/types.d.ts +67 -0
- package/dist/bridge/types.js +1 -0
- package/dist/compile/compile.d.ts +35 -0
- package/dist/compile/compile.js +30 -0
- package/dist/compile/policy.d.ts +55 -0
- package/dist/compile/policy.js +106 -0
- package/dist/config/capabilities.d.ts +154 -0
- package/dist/config/capabilities.js +63 -0
- package/dist/config/schema.d.ts +131 -0
- package/dist/config/schema.js +1 -0
- package/dist/config/validate.d.ts +20 -0
- package/dist/config/validate.js +230 -0
- package/dist/conformance/behavior.d.ts +2 -0
- package/dist/conformance/behavior.js +376 -0
- package/dist/conformance/index.d.ts +95 -0
- package/dist/conformance/index.js +4 -0
- package/dist/conformance/kit.d.ts +42 -0
- package/dist/conformance/kit.js +24 -0
- package/dist/conformance/oracles.d.ts +35 -0
- package/dist/conformance/oracles.js +77 -0
- package/dist/conformance/probes.d.ts +9 -0
- package/dist/conformance/probes.js +865 -0
- package/dist/conformance/served.d.ts +68 -0
- package/dist/conformance/served.js +311 -0
- package/dist/guide/examples.d.ts +70 -0
- package/dist/guide/examples.js +69 -0
- package/dist/guide/guide.d.ts +16 -0
- package/dist/guide/guide.js +261 -0
- package/dist/host/create-host.d.ts +8 -0
- package/dist/host/create-host.js +248 -0
- package/dist/host/locate.d.ts +8 -0
- package/dist/host/locate.js +11 -0
- package/dist/host/types.d.ts +95 -0
- package/dist/host/types.js +1 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +19 -0
- package/dist/isolation/served.d.ts +32 -0
- package/dist/isolation/served.js +97 -0
- package/dist/isolation/types.d.ts +87 -0
- package/dist/isolation/types.js +1 -0
- package/dist/mcp/adapter.d.ts +43 -0
- package/dist/mcp/adapter.js +327 -0
- package/dist/mcp/index.d.ts +27 -0
- package/dist/mcp/index.js +25 -0
- package/dist/mcp/proxy.d.ts +25 -0
- package/dist/mcp/proxy.js +106 -0
- package/dist/mcp/view.d.ts +31 -0
- package/dist/mcp/view.js +72 -0
- package/dist/platform/types.d.ts +44 -0
- package/dist/platform/types.js +1 -0
- package/dist/platform/web.d.ts +8 -0
- package/dist/platform/web.js +72 -0
- package/dist/presets/presets.d.ts +65 -0
- package/dist/presets/presets.js +125 -0
- package/dist/protocol/adapter.d.ts +73 -0
- package/dist/protocol/adapter.js +1 -0
- package/dist/protocol/jsonrpc.d.ts +43 -0
- package/dist/protocol/jsonrpc.js +50 -0
- package/dist/protocol/mcp-apps.d.ts +79 -0
- package/dist/protocol/mcp-apps.js +99 -0
- package/dist/protocol/shell-adapter.d.ts +5 -0
- package/dist/protocol/shell-adapter.js +228 -0
- package/dist/react/dialog.d.ts +20 -0
- package/dist/react/dialog.js +98 -0
- package/dist/react/frame.d.ts +75 -0
- package/dist/react/frame.js +221 -0
- package/dist/react/index.d.ts +13 -0
- package/dist/react/index.js +13 -0
- package/dist/react/live.d.ts +24 -0
- package/dist/react/live.js +57 -0
- package/dist/server/index.d.ts +27 -0
- package/dist/server/index.js +26 -0
- package/dist/server/keys.d.ts +19 -0
- package/dist/server/keys.js +78 -0
- package/dist/server/pages.d.ts +6 -0
- package/dist/server/pages.js +20 -0
- package/dist/server/proxy.d.ts +21 -0
- package/dist/server/proxy.js +107 -0
- package/dist/server/server.d.ts +7 -0
- package/dist/server/server.js +518 -0
- package/dist/server/tickets.d.ts +8 -0
- package/dist/server/tickets.js +28 -0
- package/dist/server/types.d.ts +97 -0
- package/dist/server/types.js +1 -0
- package/dist/shell/document.d.ts +28 -0
- package/dist/shell/document.js +84 -0
- package/dist/shell/plan.d.ts +35 -0
- package/dist/shell/plan.js +34 -0
- package/dist/shell/runtime.d.ts +28 -0
- package/dist/shell/runtime.js +832 -0
- package/dist/theme/variables.d.ts +73 -0
- package/dist/theme/variables.js +171 -0
- package/dist/transport/post-message.d.ts +9 -0
- package/dist/transport/post-message.js +38 -0
- package/dist/transport/types.d.ts +15 -0
- package/dist/transport/types.js +1 -0
- package/package.json +71 -1
- package/src/bridge/bridge.ts +256 -0
- package/src/bridge/types.ts +77 -0
- package/src/compile/compile.ts +68 -0
- package/src/compile/policy.ts +133 -0
- package/src/config/capabilities.ts +138 -0
- package/src/config/schema.ts +140 -0
- package/src/config/validate.ts +284 -0
- package/src/conformance/behavior.ts +430 -0
- package/src/conformance/index.ts +102 -0
- package/src/conformance/kit.ts +63 -0
- package/src/conformance/oracles.ts +126 -0
- package/src/conformance/probes.ts +1093 -0
- package/src/conformance/served.ts +472 -0
- package/src/guide/examples.ts +74 -0
- package/src/guide/guide.ts +424 -0
- package/src/host/create-host.ts +290 -0
- package/src/host/locate.ts +20 -0
- package/src/host/types.ts +95 -0
- package/src/index.ts +118 -0
- package/src/isolation/served.ts +127 -0
- package/src/isolation/types.ts +77 -0
- package/src/mcp/adapter.ts +441 -0
- package/src/mcp/index.ts +50 -0
- package/src/mcp/proxy.ts +117 -0
- package/src/mcp/view.ts +97 -0
- package/src/platform/types.ts +48 -0
- package/src/platform/web.ts +78 -0
- package/src/presets/presets.ts +193 -0
- package/src/protocol/adapter.ts +67 -0
- package/src/protocol/jsonrpc.ts +83 -0
- package/src/protocol/mcp-apps.ts +138 -0
- package/src/protocol/shell-adapter.ts +261 -0
- package/src/react/dialog.tsx +158 -0
- package/src/react/frame.tsx +422 -0
- package/src/react/index.ts +20 -0
- package/src/react/live.ts +76 -0
- package/src/server/index.ts +37 -0
- package/src/server/keys.ts +100 -0
- package/src/server/pages.ts +31 -0
- package/src/server/proxy.ts +131 -0
- package/src/server/server.ts +617 -0
- package/src/server/tickets.ts +30 -0
- package/src/server/types.ts +104 -0
- package/src/shell/document.ts +120 -0
- package/src/shell/plan.ts +66 -0
- package/src/shell/runtime.ts +832 -0
- package/src/theme/variables.ts +190 -0
- package/src/transport/post-message.ts +45 -0
- package/src/transport/types.ts +16 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { CAPABILITIES } from "../config/capabilities.js";
|
|
2
|
+
/** The capabilities a config declares, in vocabulary order. */
|
|
3
|
+
export function declaredCapabilities(config) {
|
|
4
|
+
return Object.keys(CAPABILITIES).filter((name) => config.capabilities[name] !== undefined);
|
|
5
|
+
}
|
|
6
|
+
/** The gates a declared capability runs under: its own, else the vocabulary's defaults. */
|
|
7
|
+
export function effectiveGates(config, name) {
|
|
8
|
+
return config.capabilities[name]?.gates ?? CAPABILITIES[name].defaultGates;
|
|
9
|
+
}
|
|
10
|
+
const unique = (values) => [...new Set(values)];
|
|
11
|
+
/** The origins content may reach for `kind`: the base allowlist plus every declared capability's. */
|
|
12
|
+
export function egressOrigins(config, kind) {
|
|
13
|
+
const fromCapabilities = declaredCapabilities(config).flatMap((name) => {
|
|
14
|
+
const capability = config.capabilities[name];
|
|
15
|
+
return (kind === "connect" ? capability?.connect : capability?.resource) ?? [];
|
|
16
|
+
});
|
|
17
|
+
return unique([...(config.egress[kind] ?? []), ...fromCapabilities]);
|
|
18
|
+
}
|
|
19
|
+
/** Every origin content may reach, of any kind. */
|
|
20
|
+
export function allEgressOrigins(config) {
|
|
21
|
+
const kinds = ["connect", "script", "style", "img", "font", "media"];
|
|
22
|
+
return unique(kinds.flatMap((kind) => egressOrigins(config, kind)));
|
|
23
|
+
}
|
|
24
|
+
const list = (sources) => sources.length === 0 ? ["'none'"] : [...sources];
|
|
25
|
+
/**
|
|
26
|
+
* The frame's CSP directives. Closed by default: `default-src 'none'`, and each kind opened only
|
|
27
|
+
* to the config's origins. Inline code runs (agent content is inline), so the walls are origin
|
|
28
|
+
* isolation and egress, not script provenance. No `'self'` anywhere: an opaque frame has nothing
|
|
29
|
+
* of its own to load, and its `'self'` may be the embedding page's origin. Empty for url mode,
|
|
30
|
+
* whose app serves its own policy.
|
|
31
|
+
*/
|
|
32
|
+
export function frameCspDirectives(config) {
|
|
33
|
+
if (config.mode === "url") {
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
const origins = (kind) => egressOrigins(config, kind);
|
|
37
|
+
return [
|
|
38
|
+
["default-src", "'none'"],
|
|
39
|
+
["script-src", "'unsafe-inline'", "'unsafe-eval'", "blob:", ...origins("script")],
|
|
40
|
+
["style-src", "'unsafe-inline'", ...origins("style")],
|
|
41
|
+
["font-src", "data:", ...origins("font")],
|
|
42
|
+
["img-src", "data:", "blob:", ...origins("img")],
|
|
43
|
+
["media-src", "data:", "blob:", ...origins("media")],
|
|
44
|
+
["connect-src", ...list(origins("connect"))],
|
|
45
|
+
["worker-src", "blob:"],
|
|
46
|
+
["child-src", "blob:"],
|
|
47
|
+
["frame-src", "'none'"],
|
|
48
|
+
["object-src", "'none'"],
|
|
49
|
+
["base-uri", "'none'"],
|
|
50
|
+
["form-action", "'none'"],
|
|
51
|
+
// WebRTC reaches the network outside connect-src (ICE gathering against a STUN/TURN server).
|
|
52
|
+
// Engines that honor this token block it; the shell removes the RTC constructors for the rest.
|
|
53
|
+
["webrtc", "'block'"],
|
|
54
|
+
];
|
|
55
|
+
}
|
|
56
|
+
export function serializeCsp(directives) {
|
|
57
|
+
return directives.map((directive) => directive.join(" ")).join("; ");
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The frame's `sandbox` tokens. Opaque: scripts only, so the frame is an opaque origin and gets
|
|
61
|
+
* no forms, popups, downloads, modals or top navigation. Domain: its own origin as well, which is
|
|
62
|
+
* safe only because that origin is a separate site from the host's (the mount refuses one that
|
|
63
|
+
* isn't). External (url mode): what a running web app needs to work on its own origin.
|
|
64
|
+
*/
|
|
65
|
+
export function sandboxTokens(config) {
|
|
66
|
+
switch (config.isolation) {
|
|
67
|
+
case "opaque":
|
|
68
|
+
return ["allow-scripts"];
|
|
69
|
+
case "domain":
|
|
70
|
+
return ["allow-scripts", "allow-same-origin"];
|
|
71
|
+
case "external":
|
|
72
|
+
return ["allow-scripts", "allow-same-origin", "allow-forms"];
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* The frame's `allow` attribute (Permissions Policy). Empty: no camera, microphone, geolocation,
|
|
77
|
+
* clipboard, fullscreen or other powerful feature is delegated to the frame.
|
|
78
|
+
*/
|
|
79
|
+
export function allowAttribute(_config) {
|
|
80
|
+
return "";
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* The `frame-src` sources the EMBEDDING page must allow, and nothing else: a frame's own CSP can't
|
|
84
|
+
* stop it navigating itself to an external URL with data in the query, but the embedding page's
|
|
85
|
+
* `frame-src` governs every navigation of its frames, including self-initiated ones. `srcdoc`
|
|
86
|
+
* frames need no source (measured: they load under `frame-src 'none'`), so opaque content gets
|
|
87
|
+
* none, and can't navigate its frame even to the host's own origin (P033); served and external
|
|
88
|
+
* content adds its own origins, and a host its own frames (`'self'` only if it names it).
|
|
89
|
+
*/
|
|
90
|
+
export function embedderFrameSources(config) {
|
|
91
|
+
return unique([...config.contentOrigins, ...(config.embedder.frameSrc ?? [])]);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* The CSP the embedding page sends (a header, or the host's own document's policy), for every kind
|
|
95
|
+
* of display it shows. It sets no `default-src`: it restricts frames only, leaving the host's own
|
|
96
|
+
* scripts, styles and fetches to the host's policy. `child-src` repeats `frame-src` for engines
|
|
97
|
+
* that read frames from it.
|
|
98
|
+
*/
|
|
99
|
+
export function embedderCsp(configs) {
|
|
100
|
+
const sources = unique(configs.flatMap(embedderFrameSources));
|
|
101
|
+
const all = sources.length === 0 ? ["'none'"] : sources;
|
|
102
|
+
return serializeCsp([
|
|
103
|
+
["frame-src", ...all],
|
|
104
|
+
["child-src", ...all],
|
|
105
|
+
]);
|
|
106
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The capability vocabulary (PRINCIPLE 4): every power content has beyond drawing is one of these
|
|
3
|
+
* host functions, named in the config. A capability the config doesn't declare is not wired into
|
|
4
|
+
* the shell, not carried by the bridge, adds nothing to the CSP, and is absent from the manual.
|
|
5
|
+
*
|
|
6
|
+
* Each has default gates, applied when a config declares it without its own (PRINCIPLE 1: gates
|
|
7
|
+
* are on by default), and is served either by a handler the host passes or as an event the
|
|
8
|
+
* framework emits.
|
|
9
|
+
*/
|
|
10
|
+
export declare const CAPABILITY_NAMES: readonly ["sendPrompt", "openLink", "downloadFile", "callTool", "readData", "setModelContext", "reportError", "resize"];
|
|
11
|
+
export type CapabilityName = (typeof CAPABILITY_NAMES)[number];
|
|
12
|
+
/**
|
|
13
|
+
* What must hold before a call reaches the host's handler. A capability's gates all apply.
|
|
14
|
+
*
|
|
15
|
+
* - `user-activation`: the call follows a real user gesture in the frame. The shell refuses a call
|
|
16
|
+
* without one, and the host checks its own page's activation (which a gesture in a child frame
|
|
17
|
+
* propagates to). It stops calls made on load or on a timer; it is not proof of intent.
|
|
18
|
+
* - `confirm`: the host's `confirm` function shows a person the call's whole effect, and the call
|
|
19
|
+
* goes only if they accept. No `confirm` function means the call is refused (fail closed).
|
|
20
|
+
* - `rateLimit`: at most `perMinute` calls per display per minute; the rest are refused.
|
|
21
|
+
*/
|
|
22
|
+
export type Gate = "user-activation" | "confirm" | {
|
|
23
|
+
readonly rateLimit: {
|
|
24
|
+
readonly perMinute: number;
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
export interface CapabilitySpec {
|
|
28
|
+
/** The gates a declared capability gets when its config names none. */
|
|
29
|
+
readonly defaultGates: readonly Gate[];
|
|
30
|
+
/** `handler`: the host passes a function that performs it. `event`: the framework emits it. */
|
|
31
|
+
readonly served: "handler" | "event";
|
|
32
|
+
/** The global the shell defines for it (sugar over the protocol message), if any. */
|
|
33
|
+
readonly global?: string;
|
|
34
|
+
/**
|
|
35
|
+
* How content reaches it without the shell's global: the MCP Apps request, and the official
|
|
36
|
+
* App SDK's method for it. Absent for what the shell does by itself (sizes, errors).
|
|
37
|
+
*/
|
|
38
|
+
readonly request?: {
|
|
39
|
+
readonly method: string;
|
|
40
|
+
readonly sdk: string;
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
export declare const CAPABILITIES: {
|
|
44
|
+
readonly sendPrompt: {
|
|
45
|
+
readonly defaultGates: readonly ["user-activation", "confirm"];
|
|
46
|
+
readonly served: "handler";
|
|
47
|
+
readonly global: "sendPrompt";
|
|
48
|
+
readonly request: {
|
|
49
|
+
readonly method: "ui/message";
|
|
50
|
+
readonly sdk: "app.sendMessage";
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
readonly openLink: {
|
|
54
|
+
readonly defaultGates: readonly ["user-activation", "confirm"];
|
|
55
|
+
readonly served: "handler";
|
|
56
|
+
readonly global: "openLink";
|
|
57
|
+
readonly request: {
|
|
58
|
+
readonly method: "ui/open-link";
|
|
59
|
+
readonly sdk: "app.openLink";
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
readonly downloadFile: {
|
|
63
|
+
readonly defaultGates: readonly ["user-activation", "confirm"];
|
|
64
|
+
readonly served: "handler";
|
|
65
|
+
readonly request: {
|
|
66
|
+
readonly method: "ui/download-file";
|
|
67
|
+
readonly sdk: "app.downloadFile";
|
|
68
|
+
};
|
|
69
|
+
};
|
|
70
|
+
readonly callTool: {
|
|
71
|
+
readonly defaultGates: readonly ["confirm"];
|
|
72
|
+
readonly served: "handler";
|
|
73
|
+
readonly request: {
|
|
74
|
+
readonly method: "tools/call";
|
|
75
|
+
readonly sdk: "app.callServerTool";
|
|
76
|
+
};
|
|
77
|
+
};
|
|
78
|
+
readonly readData: {
|
|
79
|
+
readonly defaultGates: readonly [Gate];
|
|
80
|
+
readonly served: "handler";
|
|
81
|
+
readonly request: {
|
|
82
|
+
readonly method: "resources/read";
|
|
83
|
+
readonly sdk: "app.readServerResource";
|
|
84
|
+
};
|
|
85
|
+
};
|
|
86
|
+
readonly setModelContext: {
|
|
87
|
+
readonly defaultGates: readonly [Gate];
|
|
88
|
+
readonly served: "handler";
|
|
89
|
+
readonly request: {
|
|
90
|
+
readonly method: "ui/update-model-context";
|
|
91
|
+
readonly sdk: "app.updateModelContext";
|
|
92
|
+
};
|
|
93
|
+
};
|
|
94
|
+
readonly reportError: {
|
|
95
|
+
readonly defaultGates: readonly [Gate];
|
|
96
|
+
readonly served: "event";
|
|
97
|
+
};
|
|
98
|
+
readonly resize: {
|
|
99
|
+
readonly defaultGates: readonly [];
|
|
100
|
+
readonly served: "event";
|
|
101
|
+
};
|
|
102
|
+
};
|
|
103
|
+
/** The arguments a call carries, once the adapter has validated them. */
|
|
104
|
+
export interface CapabilityArgs {
|
|
105
|
+
readonly sendPrompt: {
|
|
106
|
+
readonly text: string;
|
|
107
|
+
};
|
|
108
|
+
readonly openLink: {
|
|
109
|
+
readonly url: string;
|
|
110
|
+
};
|
|
111
|
+
/** MCP resource contents (embedded or linked), as the protocol carries them. */
|
|
112
|
+
readonly downloadFile: {
|
|
113
|
+
readonly contents: readonly unknown[];
|
|
114
|
+
};
|
|
115
|
+
readonly callTool: {
|
|
116
|
+
readonly name: string;
|
|
117
|
+
readonly arguments: Readonly<Record<string, unknown>>;
|
|
118
|
+
};
|
|
119
|
+
readonly readData: {
|
|
120
|
+
readonly uri: string;
|
|
121
|
+
};
|
|
122
|
+
readonly setModelContext: {
|
|
123
|
+
readonly content?: readonly unknown[];
|
|
124
|
+
readonly structuredContent?: Readonly<Record<string, unknown>>;
|
|
125
|
+
};
|
|
126
|
+
readonly reportError: {
|
|
127
|
+
readonly message: string;
|
|
128
|
+
};
|
|
129
|
+
readonly resize: {
|
|
130
|
+
readonly height: number;
|
|
131
|
+
readonly width?: number;
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
/** What a call resolves to. Protocol-shaped results (a tool's result) stay opaque to the core. */
|
|
135
|
+
export interface CapabilityResults {
|
|
136
|
+
/** Whether the prompt was sent. */
|
|
137
|
+
readonly sendPrompt: boolean;
|
|
138
|
+
/** Whether the link was opened. */
|
|
139
|
+
readonly openLink: boolean;
|
|
140
|
+
/** Whether the download started. */
|
|
141
|
+
readonly downloadFile: boolean;
|
|
142
|
+
readonly callTool: unknown;
|
|
143
|
+
readonly readData: unknown;
|
|
144
|
+
readonly setModelContext: undefined;
|
|
145
|
+
readonly reportError: undefined;
|
|
146
|
+
readonly resize: undefined;
|
|
147
|
+
}
|
|
148
|
+
/** The capabilities a host serves with a handler. */
|
|
149
|
+
export type HandledCapability = {
|
|
150
|
+
[N in CapabilityName]: (typeof CAPABILITIES)[N]["served"] extends "handler" ? N : never;
|
|
151
|
+
}[CapabilityName];
|
|
152
|
+
/** A capability's spec, as the general shape (the table above keeps literal types). */
|
|
153
|
+
export declare function capabilitySpec(name: CapabilityName): CapabilitySpec;
|
|
154
|
+
export declare function isCapabilityName(value: unknown): value is CapabilityName;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The capability vocabulary (PRINCIPLE 4): every power content has beyond drawing is one of these
|
|
3
|
+
* host functions, named in the config. A capability the config doesn't declare is not wired into
|
|
4
|
+
* the shell, not carried by the bridge, adds nothing to the CSP, and is absent from the manual.
|
|
5
|
+
*
|
|
6
|
+
* Each has default gates, applied when a config declares it without its own (PRINCIPLE 1: gates
|
|
7
|
+
* are on by default), and is served either by a handler the host passes or as an event the
|
|
8
|
+
* framework emits.
|
|
9
|
+
*/
|
|
10
|
+
export const CAPABILITY_NAMES = [
|
|
11
|
+
"sendPrompt",
|
|
12
|
+
"openLink",
|
|
13
|
+
"downloadFile",
|
|
14
|
+
"callTool",
|
|
15
|
+
"readData",
|
|
16
|
+
"setModelContext",
|
|
17
|
+
"reportError",
|
|
18
|
+
"resize",
|
|
19
|
+
];
|
|
20
|
+
const rate = (perMinute) => ({ rateLimit: { perMinute } });
|
|
21
|
+
export const CAPABILITIES = {
|
|
22
|
+
sendPrompt: {
|
|
23
|
+
defaultGates: ["user-activation", "confirm"],
|
|
24
|
+
served: "handler",
|
|
25
|
+
global: "sendPrompt",
|
|
26
|
+
request: { method: "ui/message", sdk: "app.sendMessage" },
|
|
27
|
+
},
|
|
28
|
+
openLink: {
|
|
29
|
+
defaultGates: ["user-activation", "confirm"],
|
|
30
|
+
served: "handler",
|
|
31
|
+
global: "openLink",
|
|
32
|
+
request: { method: "ui/open-link", sdk: "app.openLink" },
|
|
33
|
+
},
|
|
34
|
+
downloadFile: {
|
|
35
|
+
defaultGates: ["user-activation", "confirm"],
|
|
36
|
+
served: "handler",
|
|
37
|
+
request: { method: "ui/download-file", sdk: "app.downloadFile" },
|
|
38
|
+
},
|
|
39
|
+
callTool: {
|
|
40
|
+
defaultGates: ["confirm"],
|
|
41
|
+
served: "handler",
|
|
42
|
+
request: { method: "tools/call", sdk: "app.callServerTool" },
|
|
43
|
+
},
|
|
44
|
+
readData: {
|
|
45
|
+
defaultGates: [rate(120)],
|
|
46
|
+
served: "handler",
|
|
47
|
+
request: { method: "resources/read", sdk: "app.readServerResource" },
|
|
48
|
+
},
|
|
49
|
+
setModelContext: {
|
|
50
|
+
defaultGates: [rate(30)],
|
|
51
|
+
served: "handler",
|
|
52
|
+
request: { method: "ui/update-model-context", sdk: "app.updateModelContext" },
|
|
53
|
+
},
|
|
54
|
+
reportError: { defaultGates: [rate(10)], served: "event" },
|
|
55
|
+
resize: { defaultGates: [], served: "event" },
|
|
56
|
+
};
|
|
57
|
+
/** A capability's spec, as the general shape (the table above keeps literal types). */
|
|
58
|
+
export function capabilitySpec(name) {
|
|
59
|
+
return CAPABILITIES[name];
|
|
60
|
+
}
|
|
61
|
+
export function isCapabilityName(value) {
|
|
62
|
+
return typeof value === "string" && CAPABILITY_NAMES.includes(value);
|
|
63
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import type { CapabilityName, Gate } from "./capabilities.ts";
|
|
2
|
+
/**
|
|
3
|
+
* One config per kind of display (PRINCIPLE 3). The compiler derives the frame's CSP, its sandbox
|
|
4
|
+
* and `allow` attributes, the embedding page's frame policy, the shell's wiring, and the agent
|
|
5
|
+
* manual and tool description from it, together. A preset is a config with safe defaults filled
|
|
6
|
+
* in; hosts adopt one and override theme values and callbacks.
|
|
7
|
+
*/
|
|
8
|
+
/** What is shown. */
|
|
9
|
+
export type DisplayMode =
|
|
10
|
+
/** An HTML fragment or SVG the host has (an agent wrote it), drawn by the shell. */
|
|
11
|
+
"inline"
|
|
12
|
+
/** A whole HTML document: a standalone page. */
|
|
13
|
+
| "artifact"
|
|
14
|
+
/** A standard MCP Apps view: a `ui://` template, its tool input and result. */
|
|
15
|
+
| "mcp-app"
|
|
16
|
+
/** A running app by URL (a dev server's preview): frame, isolation and access auth only. */
|
|
17
|
+
| "url";
|
|
18
|
+
/** How the content is kept away from the host. */
|
|
19
|
+
export type IsolationKind =
|
|
20
|
+
/** A sandboxed `srcdoc` frame without `allow-same-origin`: an opaque origin, no storage. */
|
|
21
|
+
"opaque"
|
|
22
|
+
/**
|
|
23
|
+
* A document served on its own subdomain of a separate registrable domain (`/server`): a real,
|
|
24
|
+
* stable origin per storage scope, its CSP in headers, private content behind a ticket.
|
|
25
|
+
*/
|
|
26
|
+
| "domain"
|
|
27
|
+
/** Someone else's origin (url mode): the frame loads it as is; there is no shell. */
|
|
28
|
+
| "external";
|
|
29
|
+
export type ColorScheme = "light" | "dark";
|
|
30
|
+
/** A theme variable's value: one for both schemes, or one per scheme. */
|
|
31
|
+
export type ThemeValue = string | {
|
|
32
|
+
readonly light: string;
|
|
33
|
+
readonly dark: string;
|
|
34
|
+
};
|
|
35
|
+
export interface CapabilityConfig {
|
|
36
|
+
/** Every gate must pass. Omitted: the capability's default gates. `[]`: ungated. */
|
|
37
|
+
readonly gates?: readonly Gate[];
|
|
38
|
+
/** Extra `connect-src` origins this capability needs. */
|
|
39
|
+
readonly connect?: readonly string[];
|
|
40
|
+
/** Extra script/style/img/font/media origins this capability needs. */
|
|
41
|
+
readonly resource?: readonly string[];
|
|
42
|
+
/** Replaces the generated manual entry for this capability. */
|
|
43
|
+
readonly guide?: string;
|
|
44
|
+
}
|
|
45
|
+
/** The declared capabilities. Absent: not available, not wired, not in the CSP, not in the manual. */
|
|
46
|
+
export type CapabilityMap = {
|
|
47
|
+
readonly [N in CapabilityName]?: CapabilityConfig;
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* Where content may load from and talk to, as exact origins (`https://cdn.example.com`, or
|
|
51
|
+
* `https://*.example.com` for every subdomain). Empty everywhere is the default: egress closed.
|
|
52
|
+
* Capabilities add their own needs to these.
|
|
53
|
+
*/
|
|
54
|
+
export interface EgressConfig {
|
|
55
|
+
readonly connect?: readonly string[];
|
|
56
|
+
readonly script?: readonly string[];
|
|
57
|
+
readonly style?: readonly string[];
|
|
58
|
+
readonly img?: readonly string[];
|
|
59
|
+
readonly font?: readonly string[];
|
|
60
|
+
readonly media?: readonly string[];
|
|
61
|
+
}
|
|
62
|
+
export interface ThemeConfig {
|
|
63
|
+
/**
|
|
64
|
+
* Every CSS variable the content may read, with its default per scheme. The shell injects
|
|
65
|
+
* exactly these names (the host supplies live values for them) and the manual teaches them.
|
|
66
|
+
*/
|
|
67
|
+
readonly variables: Readonly<Record<string, ThemeValue>>;
|
|
68
|
+
/** The window event the shell fires after the theme changed, for code that must redraw. */
|
|
69
|
+
readonly event: string;
|
|
70
|
+
}
|
|
71
|
+
export interface Limits {
|
|
72
|
+
/** The tallest the frame grows before its content scrolls inside it. */
|
|
73
|
+
readonly maxHeightPx: number;
|
|
74
|
+
/** The shortest the frame is once its content reported a size. */
|
|
75
|
+
readonly minHeightPx: number;
|
|
76
|
+
/** The largest content the manual allows (the host enforces it where it takes content in). */
|
|
77
|
+
readonly maxBytes: number;
|
|
78
|
+
/** The most frames of this kind a page runs at once (the React binding's live-frame cap). */
|
|
79
|
+
readonly maxLiveFrames: number;
|
|
80
|
+
/** Declined confirmations after which a display stops asking until it runs again. */
|
|
81
|
+
readonly declinesBeforeMute: number;
|
|
82
|
+
/** The longest prompt `sendPrompt` carries. */
|
|
83
|
+
readonly maxPromptChars: number;
|
|
84
|
+
/** The most errors one run of a display reports. */
|
|
85
|
+
readonly maxErrorReports: number;
|
|
86
|
+
}
|
|
87
|
+
/** Where the host shows the content, for the manual's size rules. */
|
|
88
|
+
export interface LayoutConfig {
|
|
89
|
+
/** The narrowest the frame ever is (a small phone). */
|
|
90
|
+
readonly minWidthPx: number;
|
|
91
|
+
/** A typical phone's width: what the content is designed for first. */
|
|
92
|
+
readonly narrowWidthPx: number;
|
|
93
|
+
/** The widest the frame is (a desktop column). */
|
|
94
|
+
readonly wideWidthPx: number;
|
|
95
|
+
}
|
|
96
|
+
export interface GuideConfig {
|
|
97
|
+
/** The host's tool that shows this content, as the manual names it (`show_widget`). */
|
|
98
|
+
readonly toolName?: string;
|
|
99
|
+
/** The host's tool that answers this manual, as the tool description names it. */
|
|
100
|
+
readonly guideToolName?: string;
|
|
101
|
+
/**
|
|
102
|
+
* The host's own topics, by name, appended to the manual when asked for (a host's file tool,
|
|
103
|
+
* say). They may not name a built-in topic. Write them about the host's tools, not the sandbox:
|
|
104
|
+
* the sandbox's rules are generated, so they always match it.
|
|
105
|
+
*/
|
|
106
|
+
readonly topics?: Readonly<Record<string, string>>;
|
|
107
|
+
}
|
|
108
|
+
export interface EmbedderConfig {
|
|
109
|
+
/**
|
|
110
|
+
* Extra `frame-src` sources the embedding page allows, for frames it shows besides these:
|
|
111
|
+
* origins, or `'self'` if it frames its own pages (which lets content navigate there too).
|
|
112
|
+
*/
|
|
113
|
+
readonly frameSrc?: readonly string[];
|
|
114
|
+
}
|
|
115
|
+
export interface PortholeConfig {
|
|
116
|
+
readonly mode: DisplayMode;
|
|
117
|
+
readonly isolation: IsolationKind;
|
|
118
|
+
readonly capabilities: CapabilityMap;
|
|
119
|
+
readonly egress: EgressConfig;
|
|
120
|
+
readonly theme: ThemeConfig;
|
|
121
|
+
readonly limits: Limits;
|
|
122
|
+
readonly layout: LayoutConfig;
|
|
123
|
+
readonly guide: GuideConfig;
|
|
124
|
+
/**
|
|
125
|
+
* Where `domain` and `external` content is served from, as frame-src sources
|
|
126
|
+
* (`https://*.example-usercontent.com`; dev `http://*.localhost:8790`): the embedding page frames
|
|
127
|
+
* them, and the host takes messages only from a frame whose origin matches. Empty for `opaque`.
|
|
128
|
+
*/
|
|
129
|
+
readonly contentOrigins: readonly string[];
|
|
130
|
+
readonly embedder: EmbedderConfig;
|
|
131
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { PortholeConfig } from "./schema.ts";
|
|
2
|
+
/**
|
|
3
|
+
* Checks a config before anything is derived from it. Every string that ends up in a CSP, an
|
|
4
|
+
* attribute or the shell is checked here, so a malformed origin can't inject a directive
|
|
5
|
+
* (`https://a.com; script-src *`) and a typo can't widen a policy. A config that fails is refused
|
|
6
|
+
* whole, never repaired.
|
|
7
|
+
*/
|
|
8
|
+
export declare class ConfigError extends Error {
|
|
9
|
+
readonly path: string;
|
|
10
|
+
constructor(path: string, message: string);
|
|
11
|
+
}
|
|
12
|
+
/** The built-in manual topics; a host's own topics may not reuse these names. */
|
|
13
|
+
export declare const BUILT_IN_TOPICS: readonly ["widget", "chart", "interactive"];
|
|
14
|
+
/**
|
|
15
|
+
* Whether `value` is a CSP host source Porthole accepts: a scheme, a host (optionally `*.` for
|
|
16
|
+
* every subdomain) and an optional port, nothing else. Insecure schemes only for loopback names.
|
|
17
|
+
*/
|
|
18
|
+
export declare function isOriginSource(value: unknown): value is string;
|
|
19
|
+
/** Returns `input` as a config once every field checks out; throws a `ConfigError` otherwise. */
|
|
20
|
+
export declare function validateConfig(input: unknown): PortholeConfig;
|