@guuey/mcp-apps-host 0.7.1 → 0.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/react.d.ts +9 -6
- package/dist/react.d.ts.map +1 -1
- package/dist/react.js +18 -2
- package/dist/view-host-protocol.d.ts +49 -1
- package/dist/view-host-protocol.d.ts.map +1 -1
- package/dist/view-host-protocol.js +56 -0
- package/dist/view-host.d.ts +45 -1
- package/dist/view-host.d.ts.map +1 -1
- package/dist/view-host.js +32 -1
- package/package.json +1 -1
- package/src/index.ts +5 -0
- package/src/react.tsx +36 -7
- package/src/view-host-protocol.ts +102 -0
- package/src/view-host.ts +77 -0
package/dist/index.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ export { asGguiRender, asGguiRenderBootstrap, blockGguiRender, gguiRenderResourc
|
|
|
8
8
|
export { resolveViewMount, snapshotViewMount, toolResultViewMount, type LocatorViewMount, type ResolvedViewMount, type UiResourceReader, type ViewMount, type ViewMountChannel, } from "./card-mount.js";
|
|
9
9
|
export { createMcpUiResourceReader, uiResourceChannel, type CreateMcpUiResourceReaderDeps, type McpResourceReadResult, } from "./reader.js";
|
|
10
10
|
export { asToolCallResult, createMcpUiActionRelay, unavailableToolCallResult, UI_ACTION_TOOLS, UI_ACTION_UNAVAILABLE_TEXT, UI_SEMANTIC_ACTION_TOOLS, type CreateMcpUiActionRelayDeps, type McpToolCallContent, type McpToolCallResult, type McpToolStructuredContent, type UiActionRequest, } from "./action.js";
|
|
11
|
-
export { initializeResult, initialViewHostState, resourceReadResponse, RESOURCES_READ_METHOD, teardownMessage, toolCallResponse, TOOLS_CALL_METHOD, viewHostElapsed, viewHostReceive, type ViewHostBehavior, type ViewHostEffect, type ViewHostOutbound, type ViewHostPhase, type ViewHostInfo, type ViewHostState, type ViewHostTransition, type ViewRequestId, } from "./view-host-protocol.js";
|
|
12
|
-
export { attachViewHost, viewDocumentHtml, type AttachViewHostConfig, type ViewFrameLike, type ViewHostEvents, } from "./view-host.js";
|
|
11
|
+
export { initializeResult, initialViewHostState, resourceReadResponse, RESOURCES_READ_METHOD, teardownMessage, toolCallResponse, TOOLS_CALL_METHOD, viewHostElapsed, viewHostReceive, diagnoseCspViolation, type CspViolationLike, type ViewCspDiagnosis, type ViewCspOrigins, type ViewHostBehavior, type ViewHostEffect, type ViewHostOutbound, type ViewHostPhase, type ViewHostInfo, type ViewHostState, type ViewHostTransition, type ViewRequestId, } from "./view-host-protocol.js";
|
|
12
|
+
export { attachViewHost, viewDocumentHtml, type AttachViewHostConfig, type ViewCspEvents, type ViewFrameLike, type ViewHostEvents, } from "./view-host.js";
|
|
13
13
|
export { attachSandboxPageDelivery, isSandboxProxyReady, SANDBOX_PROXY_READY_METHOD, SANDBOX_RESOURCE_READY_METHOD, type SandboxPageDeliveryConfig, } from "./sandbox-page.js";
|
|
14
14
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EACL,iBAAiB,EACjB,YAAY,EACZ,eAAe,EACf,YAAY,EACZ,YAAY,EACZ,4BAA4B,EAC5B,kBAAkB,EAClB,iBAAiB,EACjB,oBAAoB,EACpB,SAAS,EACT,KAAK,oBAAoB,GAC1B,MAAM,eAAe,CAAC;AAMvB,OAAO,EACL,YAAY,EACZ,qBAAqB,EACrB,eAAe,EACf,kBAAkB,EAClB,aAAa,EACb,oBAAoB,EACpB,oBAAoB,EACpB,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,EACzB,KAAK,oBAAoB,GAC1B,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,gBAAgB,EAChB,iBAAiB,EACjB,mBAAmB,EACnB,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,EACrB,KAAK,SAAS,EACd,KAAK,gBAAgB,GACtB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,yBAAyB,EACzB,iBAAiB,EACjB,KAAK,6BAA6B,EAClC,KAAK,qBAAqB,GAC3B,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,gBAAgB,EAChB,sBAAsB,EACtB,yBAAyB,EACzB,eAAe,EACf,0BAA0B,EAC1B,wBAAwB,EACxB,KAAK,0BAA0B,EAC/B,KAAK,kBAAkB,EACvB,KAAK,iBAAiB,EACtB,KAAK,wBAAwB,EAC7B,KAAK,eAAe,GACrB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,gBAAgB,EAChB,oBAAoB,EACpB,oBAAoB,EACpB,qBAAqB,EACrB,eAAe,EACf,gBAAgB,EAChB,iBAAiB,EACjB,eAAe,EACf,eAAe,EACf,KAAK,gBAAgB,EACrB,KAAK,cAAc,EACnB,KAAK,gBAAgB,EACrB,KAAK,aAAa,EAClB,KAAK,YAAY,EACjB,KAAK,aAAa,EAClB,KAAK,kBAAkB,EACvB,KAAK,aAAa,GACnB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EACL,cAAc,EACd,gBAAgB,EAChB,KAAK,oBAAoB,EACzB,KAAK,aAAa,EAClB,KAAK,cAAc,GACpB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,yBAAyB,EACzB,mBAAmB,EACnB,0BAA0B,EAC1B,6BAA6B,EAC7B,KAAK,yBAAyB,GAC/B,MAAM,mBAAmB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EACL,iBAAiB,EACjB,YAAY,EACZ,eAAe,EACf,YAAY,EACZ,YAAY,EACZ,4BAA4B,EAC5B,kBAAkB,EAClB,iBAAiB,EACjB,oBAAoB,EACpB,SAAS,EACT,KAAK,oBAAoB,GAC1B,MAAM,eAAe,CAAC;AAMvB,OAAO,EACL,YAAY,EACZ,qBAAqB,EACrB,eAAe,EACf,kBAAkB,EAClB,aAAa,EACb,oBAAoB,EACpB,oBAAoB,EACpB,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,EACzB,KAAK,oBAAoB,GAC1B,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,gBAAgB,EAChB,iBAAiB,EACjB,mBAAmB,EACnB,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,EACrB,KAAK,SAAS,EACd,KAAK,gBAAgB,GACtB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,yBAAyB,EACzB,iBAAiB,EACjB,KAAK,6BAA6B,EAClC,KAAK,qBAAqB,GAC3B,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,gBAAgB,EAChB,sBAAsB,EACtB,yBAAyB,EACzB,eAAe,EACf,0BAA0B,EAC1B,wBAAwB,EACxB,KAAK,0BAA0B,EAC/B,KAAK,kBAAkB,EACvB,KAAK,iBAAiB,EACtB,KAAK,wBAAwB,EAC7B,KAAK,eAAe,GACrB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,gBAAgB,EAChB,oBAAoB,EACpB,oBAAoB,EACpB,qBAAqB,EACrB,eAAe,EACf,gBAAgB,EAChB,iBAAiB,EACjB,eAAe,EACf,eAAe,EACf,oBAAoB,EACpB,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,EACrB,KAAK,cAAc,EACnB,KAAK,gBAAgB,EACrB,KAAK,cAAc,EACnB,KAAK,gBAAgB,EACrB,KAAK,aAAa,EAClB,KAAK,YAAY,EACjB,KAAK,aAAa,EAClB,KAAK,kBAAkB,EACvB,KAAK,aAAa,GACnB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EACL,cAAc,EACd,gBAAgB,EAChB,KAAK,oBAAoB,EACzB,KAAK,aAAa,EAClB,KAAK,aAAa,EAClB,KAAK,cAAc,GACpB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,yBAAyB,EACzB,mBAAmB,EACnB,0BAA0B,EAC1B,6BAA6B,EAC7B,KAAK,yBAAyB,GAC/B,MAAM,mBAAmB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -13,6 +13,6 @@ export { asGguiRender, asGguiRenderBootstrap, blockGguiRender, gguiRenderResourc
|
|
|
13
13
|
export { resolveViewMount, snapshotViewMount, toolResultViewMount, } from "./card-mount.js";
|
|
14
14
|
export { createMcpUiResourceReader, uiResourceChannel, } from "./reader.js";
|
|
15
15
|
export { asToolCallResult, createMcpUiActionRelay, unavailableToolCallResult, UI_ACTION_TOOLS, UI_ACTION_UNAVAILABLE_TEXT, UI_SEMANTIC_ACTION_TOOLS, } from "./action.js";
|
|
16
|
-
export { initializeResult, initialViewHostState, resourceReadResponse, RESOURCES_READ_METHOD, teardownMessage, toolCallResponse, TOOLS_CALL_METHOD, viewHostElapsed, viewHostReceive, } from "./view-host-protocol.js";
|
|
16
|
+
export { initializeResult, initialViewHostState, resourceReadResponse, RESOURCES_READ_METHOD, teardownMessage, toolCallResponse, TOOLS_CALL_METHOD, viewHostElapsed, viewHostReceive, diagnoseCspViolation, } from "./view-host-protocol.js";
|
|
17
17
|
export { attachViewHost, viewDocumentHtml, } from "./view-host.js";
|
|
18
18
|
export { attachSandboxPageDelivery, isSandboxProxyReady, SANDBOX_PROXY_READY_METHOD, SANDBOX_RESOURCE_READY_METHOD, } from "./sandbox-page.js";
|
package/dist/react.d.ts
CHANGED
|
@@ -34,14 +34,14 @@
|
|
|
34
34
|
*/
|
|
35
35
|
import { type CSSProperties, type ReactNode } from "react";
|
|
36
36
|
import { type AttachViewHostConfig } from "./view-host.js";
|
|
37
|
-
import type { ViewHostPhase } from "./view-host-protocol.js";
|
|
37
|
+
import type { ViewCspDiagnosis, ViewHostPhase } from "./view-host-protocol.js";
|
|
38
38
|
import type { ResolvedViewMount } from "./card-mount.js";
|
|
39
39
|
export { attachViewHost, viewDocumentHtml } from "./view-host.js";
|
|
40
|
-
export type { AttachViewHostConfig, ViewFrameLike, ViewHostEvents } from "./view-host.js";
|
|
40
|
+
export type { AttachViewHostConfig, ViewCspEvents, ViewFrameLike, ViewHostEvents } from "./view-host.js";
|
|
41
41
|
export { attachSandboxPageDelivery, isSandboxProxyReady, SANDBOX_PROXY_READY_METHOD, SANDBOX_RESOURCE_READY_METHOD, type SandboxPageDeliveryConfig, } from "./sandbox-page.js";
|
|
42
|
-
export type { ViewHostPhase } from "./view-host-protocol.js";
|
|
42
|
+
export type { ViewCspDiagnosis, ViewCspOrigins, ViewHostPhase } from "./view-host-protocol.js";
|
|
43
43
|
export type { ResolvedViewMount, ViewMount, ViewMountChannel } from "./card-mount.js";
|
|
44
|
-
export interface GuueyViewProps extends Pick<AttachViewHostConfig, "hostCapabilities" | "hostInfo" | "hostContext" | "onCallTool" | "onReadResource" | "onSizeChanged" | "negotiationTimeoutMs"> {
|
|
44
|
+
export interface GuueyViewProps extends Pick<AttachViewHostConfig, "hostCapabilities" | "hostInfo" | "hostContext" | "onCallTool" | "onReadResource" | "onSizeChanged" | "negotiationTimeoutMs" | "cspOrigins" | "onCspDiagnosis"> {
|
|
45
45
|
/** The resolved card to mount (see `toolResultViewMount`/`resolveViewMount`). */
|
|
46
46
|
mount: ResolvedViewMount;
|
|
47
47
|
/**
|
|
@@ -97,9 +97,12 @@ export interface GuueyViewProps extends Pick<AttachViewHostConfig, "hostCapabili
|
|
|
97
97
|
* "render nothing". The default: a quiet "Negotiating with view…" line
|
|
98
98
|
* while `"negotiating"`; a labeled failure for `"no-handshake"` on the
|
|
99
99
|
* `"ggui"` channel; nothing once `"connected"` (the view owns its
|
|
100
|
-
* pixels) and nothing for a silent `"inline"` card.
|
|
100
|
+
* pixels) and nothing for a silent `"inline"` card. The second argument
|
|
101
|
+
* is the CSP diagnosis when the tripwire caught one (see
|
|
102
|
+
* {@link AttachViewHostConfig.cspOrigins}) — the default label folds it
|
|
103
|
+
* in; a custom renderer decides how to show it.
|
|
101
104
|
*/
|
|
102
|
-
renderStatus?: (phase: ViewHostPhase) => ReactNode;
|
|
105
|
+
renderStatus?: (phase: ViewHostPhase, diagnosis?: ViewCspDiagnosis) => ReactNode;
|
|
103
106
|
}
|
|
104
107
|
/**
|
|
105
108
|
* Mount a resolved view and play the MCP Apps Host for it. See the module
|
package/dist/react.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"react.d.ts","sourceRoot":"","sources":["../src/react.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,OAAO,EAAwC,KAAK,aAAa,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AACjG,OAAO,EAAoC,KAAK,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AAE7F,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;
|
|
1
|
+
{"version":3,"file":"react.d.ts","sourceRoot":"","sources":["../src/react.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,OAAO,EAAwC,KAAK,aAAa,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AACjG,OAAO,EAAoC,KAAK,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AAE7F,OAAO,KAAK,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAC/E,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAEzD,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAClE,YAAY,EAAE,oBAAoB,EAAE,aAAa,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AACzG,OAAO,EACL,yBAAyB,EACzB,mBAAmB,EACnB,0BAA0B,EAC1B,6BAA6B,EAC7B,KAAK,yBAAyB,GAC/B,MAAM,mBAAmB,CAAC;AAC3B,YAAY,EAAE,gBAAgB,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAC/F,YAAY,EAAE,iBAAiB,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAKtF,MAAM,WAAW,cACf,SAAQ,IAAI,CACV,oBAAoB,EAClB,kBAAkB,GAClB,UAAU,GACV,aAAa,GACb,YAAY,GACZ,gBAAgB,GAChB,eAAe,GACf,sBAAsB,GACtB,YAAY,GACZ,gBAAgB,CACnB;IACD,iFAAiF;IACjF,KAAK,EAAE,iBAAiB,CAAC;IACzB;;;;;;;;;;;;;;;;;;;;;OAqBG;IACH,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B;;;;;;;OAOG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;;;;OAMG;IACH,0BAA0B,CAAC,EAAE,MAAM,EAAE,CAAC;IACtC,8EAA8E;IAC9E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,wDAAwD;IACxD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,aAAa,CAAC;IACtB,6EAA6E;IAC7E,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAC;IAC/C;;;;;;;;;OASG;IACH,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,aAAa,EAAE,SAAS,CAAC,EAAE,gBAAgB,KAAK,SAAS,CAAC;CAClF;AA8CD;;;GAGG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,cAAc,GAAG,SAAS,CAmK1D"}
|
package/dist/react.js
CHANGED
|
@@ -51,10 +51,17 @@ const statusLineStyle = {
|
|
|
51
51
|
opacity: 0.65,
|
|
52
52
|
pointerEvents: "none",
|
|
53
53
|
};
|
|
54
|
-
function defaultStatus(phase, channel) {
|
|
54
|
+
function defaultStatus(phase, channel, diagnosis) {
|
|
55
55
|
if (phase === "negotiating") {
|
|
56
56
|
return _jsx("p", { style: statusLineStyle, children: "Negotiating with view\u2026" });
|
|
57
57
|
}
|
|
58
|
+
// A CSP diagnosis (guuey#235) is the WHY behind a silent frame — on any
|
|
59
|
+
// channel: a blocked runtime bundle never gets to negotiate, so the
|
|
60
|
+
// tripwire's verdict outranks the channel heuristic below. Actionable
|
|
61
|
+
// over accurate-but-mute: name the blocked URI and the allowance.
|
|
62
|
+
if (phase === "no-handshake" && diagnosis !== undefined) {
|
|
63
|
+
return (_jsx("p", { role: "alert", style: { ...statusLineStyle, opacity: 1, pointerEvents: "auto" }, children: diagnosis.message }));
|
|
64
|
+
}
|
|
58
65
|
if (phase === "no-handshake" && channel === "ggui") {
|
|
59
66
|
// A ggui shell negotiates unconditionally before painting, so silence
|
|
60
67
|
// here is a boot failure with no other author — label it (role=alert
|
|
@@ -71,6 +78,8 @@ export function GuueyView(props) {
|
|
|
71
78
|
const { mount, sandboxPageUrl, autoResize, dangerouslyAddSandboxFlags, allow, title, className, style, onPhaseChange, renderStatus, ...hostConfig } = props;
|
|
72
79
|
const frameRef = useRef(null);
|
|
73
80
|
const [phase, setPhase] = useState("negotiating");
|
|
81
|
+
// The CSP tripwire's verdict for THIS document, if any (guuey#235).
|
|
82
|
+
const [diagnosis, setDiagnosis] = useState(undefined);
|
|
74
83
|
// The view's own size report, applied only under `autoResize`.
|
|
75
84
|
const [reportedHeight, setReportedHeight] = useState(undefined);
|
|
76
85
|
const html = viewDocumentHtml(mount.resource);
|
|
@@ -107,6 +116,7 @@ export function GuueyView(props) {
|
|
|
107
116
|
// document boots fresh, and the previous negotiation's phase must not
|
|
108
117
|
// paper over it — nor must the previous document's reported size.
|
|
109
118
|
setPhase("negotiating");
|
|
119
|
+
setDiagnosis(undefined);
|
|
110
120
|
setReportedHeight(undefined);
|
|
111
121
|
const frame = frameRef.current;
|
|
112
122
|
if (frame === null || html === undefined)
|
|
@@ -121,6 +131,10 @@ export function GuueyView(props) {
|
|
|
121
131
|
setPhase(next);
|
|
122
132
|
latest.current.onPhaseChange?.(next);
|
|
123
133
|
},
|
|
134
|
+
onCspDiagnosis: (found) => {
|
|
135
|
+
setDiagnosis(found);
|
|
136
|
+
latest.current.hostConfig.onCspDiagnosis?.(found);
|
|
137
|
+
},
|
|
124
138
|
onSizeChanged: (size) => {
|
|
125
139
|
if (latest.current.autoResize === true && size.height !== undefined) {
|
|
126
140
|
setReportedHeight(size.height);
|
|
@@ -180,5 +194,7 @@ export function GuueyView(props) {
|
|
|
180
194
|
width: "100%",
|
|
181
195
|
height: autoResize === true && reportedHeight !== undefined ? reportedHeight : "100%",
|
|
182
196
|
border: 0,
|
|
183
|
-
} }, `${page?.href ?? "srcdoc"}::${mount.resource.uri}`), renderStatus !== undefined
|
|
197
|
+
} }, `${page?.href ?? "srcdoc"}::${mount.resource.uri}`), renderStatus !== undefined
|
|
198
|
+
? renderStatus(phase, diagnosis)
|
|
199
|
+
: defaultStatus(phase, mount.channel, diagnosis)] }));
|
|
184
200
|
}
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
* `ggui-render.ts` retired 2026-08-16 — guuey#209; that module is now
|
|
32
32
|
* deprecated re-exports only.)
|
|
33
33
|
*/
|
|
34
|
-
import { type McpUiHostCapabilities, type McpUiHostContext, type McpUiInitializeResult } from "@modelcontextprotocol/ext-apps";
|
|
34
|
+
import { type McpUiHostCapabilities, type McpUiHostContext, type McpUiInitializeResult, type McpUiResourceCsp } from "@modelcontextprotocol/ext-apps";
|
|
35
35
|
import type { McpToolStructuredContent } from "./action.js";
|
|
36
36
|
import type { McpResourceReadResult } from "./reader.js";
|
|
37
37
|
/**
|
|
@@ -209,4 +209,52 @@ export declare function viewHostReceive(state: ViewHostState, behavior: ViewHost
|
|
|
209
209
|
* one stays lapsed. The caller owns the clock — this machine has none.
|
|
210
210
|
*/
|
|
211
211
|
export declare function viewHostElapsed(state: ViewHostState): ViewHostState;
|
|
212
|
+
/**
|
|
213
|
+
* The origins a view needs the EMBEDDER's page to allow — the spec's own
|
|
214
|
+
* per-resource CSP declaration (`McpUiResourceCsp`: `connectDomains` →
|
|
215
|
+
* `connect-src`, `resourceDomains` → `script-src`/`style-src`/…,
|
|
216
|
+
* `frameDomains` → `frame-src`). This is the honest filter for the CSP
|
|
217
|
+
* tripwire: a `securitypolicyviolation` whose `blockedURI` lands on one of
|
|
218
|
+
* these hosts is a violation ABOUT the view, not about anything else the
|
|
219
|
+
* page loads. Empty/absent → nothing to match, tripwire inert.
|
|
220
|
+
*/
|
|
221
|
+
export type ViewCspOrigins = McpUiResourceCsp;
|
|
222
|
+
/**
|
|
223
|
+
* What the embedder can act on when its own CSP blocked the view: the
|
|
224
|
+
* URI the browser refused, the directive that refused it, and the entry
|
|
225
|
+
* that would allow it (the blocked URI's origin — the smallest allowance
|
|
226
|
+
* that fixes exactly this). Rides beside `"no-handshake"` — the phase
|
|
227
|
+
* stays honest ("it never negotiated"); this is WHY.
|
|
228
|
+
*/
|
|
229
|
+
export interface ViewCspDiagnosis {
|
|
230
|
+
blockedUri: string;
|
|
231
|
+
violatedDirective: string;
|
|
232
|
+
/** The origin to add under `violatedDirective` — e.g. `https://assets.mcp.example`. */
|
|
233
|
+
suggestedEntry: string;
|
|
234
|
+
/** Operator-facing sentence, ready to label. */
|
|
235
|
+
message: string;
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* The slice of a `SecurityPolicyViolationEvent` the tripwire reads —
|
|
239
|
+
* structural, so Node tests hand in plain objects (lib.dom's class is not
|
|
240
|
+
* constructible outside a browser).
|
|
241
|
+
*/
|
|
242
|
+
export interface CspViolationLike {
|
|
243
|
+
blockedURI: string;
|
|
244
|
+
/** e.g. `script-src-elem`, `connect-src`. */
|
|
245
|
+
violatedDirective: string;
|
|
246
|
+
/** The directive as the policy spelled it (`script-src` may govern `script-src-elem`). */
|
|
247
|
+
effectiveDirective?: string;
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Pure: is this violation ABOUT the view (its blocked URI lands on a
|
|
251
|
+
* declared origin), and if so, what should the embedder add?
|
|
252
|
+
*
|
|
253
|
+
* `blockedURI` is a full URL for network/script blocks; the browser sends
|
|
254
|
+
* bare tokens (`eval`, `inline`, `data`) for policy-class blocks — those
|
|
255
|
+
* carry no host and never match a declared origin, which is right: a
|
|
256
|
+
* `script-src eval` report is not the view's (see guuey#236 for the zod
|
|
257
|
+
* probe that produces exactly one such report at boot).
|
|
258
|
+
*/
|
|
259
|
+
export declare function diagnoseCspViolation(violation: CspViolationLike, origins: ViewCspOrigins | undefined): ViewCspDiagnosis | undefined;
|
|
212
260
|
//# sourceMappingURL=view-host-protocol.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"view-host-protocol.d.ts","sourceRoot":"","sources":["../src/view-host-protocol.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,OAAO,EAKL,KAAK,qBAAqB,EAC1B,KAAK,gBAAgB,EACrB,KAAK,qBAAqB,
|
|
1
|
+
{"version":3,"file":"view-host-protocol.d.ts","sourceRoot":"","sources":["../src/view-host-protocol.ts"],"names":[],"mappings":"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"}
|
|
@@ -229,3 +229,59 @@ export function viewHostReceive(state, behavior, data) {
|
|
|
229
229
|
export function viewHostElapsed(state) {
|
|
230
230
|
return state.phase === "negotiating" ? { ...state, phase: "no-handshake" } : state;
|
|
231
231
|
}
|
|
232
|
+
/** Every declared origin's HOST, wildcards (`https://*.x`) reduced to their suffix. */
|
|
233
|
+
function declaredHosts(origins) {
|
|
234
|
+
const out = [];
|
|
235
|
+
for (const list of [origins.connectDomains, origins.resourceDomains, origins.frameDomains]) {
|
|
236
|
+
for (const entry of list ?? []) {
|
|
237
|
+
// `https://*.example.com` → wildcard suffix `.example.com`
|
|
238
|
+
const wildcard = /^[a-z]+:\/\/\*\./i.exec(entry);
|
|
239
|
+
if (wildcard) {
|
|
240
|
+
out.push({ host: entry.slice(wildcard[0].length - 1).toLowerCase(), wildcard: true });
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
try {
|
|
244
|
+
out.push({ host: new URL(entry).hostname.toLowerCase(), wildcard: false });
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
// A malformed declaration is producer-side wire data; it simply
|
|
248
|
+
// never matches. The tripwire only ever ADDS a diagnosis, never
|
|
249
|
+
// blocks, so there is nothing to guard.
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return out;
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Pure: is this violation ABOUT the view (its blocked URI lands on a
|
|
257
|
+
* declared origin), and if so, what should the embedder add?
|
|
258
|
+
*
|
|
259
|
+
* `blockedURI` is a full URL for network/script blocks; the browser sends
|
|
260
|
+
* bare tokens (`eval`, `inline`, `data`) for policy-class blocks — those
|
|
261
|
+
* carry no host and never match a declared origin, which is right: a
|
|
262
|
+
* `script-src eval` report is not the view's (see guuey#236 for the zod
|
|
263
|
+
* probe that produces exactly one such report at boot).
|
|
264
|
+
*/
|
|
265
|
+
export function diagnoseCspViolation(violation, origins) {
|
|
266
|
+
if (origins === undefined)
|
|
267
|
+
return undefined;
|
|
268
|
+
let blocked;
|
|
269
|
+
try {
|
|
270
|
+
blocked = new URL(violation.blockedURI);
|
|
271
|
+
}
|
|
272
|
+
catch {
|
|
273
|
+
return undefined; // bare token (eval/inline/data/…) — not a host, not the view's
|
|
274
|
+
}
|
|
275
|
+
const host = blocked.hostname.toLowerCase();
|
|
276
|
+
const hit = declaredHosts(origins).some((d) => d.wildcard ? host.endsWith(d.host) : host === d.host);
|
|
277
|
+
if (!hit)
|
|
278
|
+
return undefined;
|
|
279
|
+
const directive = violation.effectiveDirective || violation.violatedDirective;
|
|
280
|
+
const suggestedEntry = blocked.origin;
|
|
281
|
+
return {
|
|
282
|
+
blockedUri: violation.blockedURI,
|
|
283
|
+
violatedDirective: directive,
|
|
284
|
+
suggestedEntry,
|
|
285
|
+
message: `This page's Content-Security-Policy blocks ${violation.blockedURI} (${directive}) — the view cannot start. Add \`${directive} ${suggestedEntry}\` to the page's policy.`,
|
|
286
|
+
};
|
|
287
|
+
}
|
package/dist/view-host.d.ts
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
* Seeded from ggui's console `surface-host.ts` (donated, guuey#186 audit);
|
|
28
28
|
* re-derived here against the pure machine + our own tests.
|
|
29
29
|
*/
|
|
30
|
-
import { type ViewHostPhase } from "./view-host-protocol.js";
|
|
30
|
+
import { type CspViolationLike, type ViewCspDiagnosis, type ViewCspOrigins, type ViewHostPhase } from "./view-host-protocol.js";
|
|
31
31
|
import type { McpResourceReadResult } from "./reader.js";
|
|
32
32
|
import { type McpToolCallResult, type UiActionRequest } from "./action.js";
|
|
33
33
|
import type { McpUiResourcePayload } from "./block-ui.js";
|
|
@@ -57,6 +57,15 @@ export interface ViewHostEvents {
|
|
|
57
57
|
source: unknown;
|
|
58
58
|
}) => void): void;
|
|
59
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* Where CSP violations are observed — the EMBEDDING document (the frame's
|
|
62
|
+
* blocked loads report there, not inside the opaque frame). Structural like
|
|
63
|
+
* {@link ViewHostEvents}, injectable for tests. Default: `document`.
|
|
64
|
+
*/
|
|
65
|
+
export interface ViewCspEvents {
|
|
66
|
+
addEventListener(type: "securitypolicyviolation", listener: (event: CspViolationLike) => void): void;
|
|
67
|
+
removeEventListener(type: "securitypolicyviolation", listener: (event: CspViolationLike) => void): void;
|
|
68
|
+
}
|
|
60
69
|
export interface AttachViewHostConfig {
|
|
61
70
|
/**
|
|
62
71
|
* Capabilities to advertise in the initialize result. Default: `{}` —
|
|
@@ -116,6 +125,41 @@ export interface AttachViewHostConfig {
|
|
|
116
125
|
}) => void;
|
|
117
126
|
/** Observe phase transitions (see {@link ViewHostPhase}). */
|
|
118
127
|
onPhaseChange?: (phase: ViewHostPhase) => void;
|
|
128
|
+
/**
|
|
129
|
+
* The origins this view needs the embedding PAGE's CSP to allow — the
|
|
130
|
+
* spec's per-resource declaration (`McpUiResourceCsp`). Given, the host
|
|
131
|
+
* arms a CSP tripwire for the attachment's lifetime: a
|
|
132
|
+
* `securitypolicyviolation` on the EMBEDDING document whose blocked URI
|
|
133
|
+
* lands on one of these hosts is upgraded from a silent "never
|
|
134
|
+
* negotiated" into an actionable {@link ViewCspDiagnosis} (guuey#235).
|
|
135
|
+
* The phase itself stays `"no-handshake"` — honest about WHAT happened;
|
|
136
|
+
* the diagnosis says WHY. Absent → tripwire not installed, zero behavior
|
|
137
|
+
* change (the default for every existing caller).
|
|
138
|
+
*
|
|
139
|
+
* REACH (pinned by the browser leg, `e2e/tests/sdk/view-host.spec.ts`):
|
|
140
|
+
* the listener lives on the embedding document, so it sees violations
|
|
141
|
+
* the PAGE incurs on the view's origins — a runtime bundle the page
|
|
142
|
+
* loads at page level, a live channel the page opens on the view's
|
|
143
|
+
* behalf (the shape ggui's landing tripwire exercised). It does NOT see
|
|
144
|
+
* a `srcdoc` view's own blocked loads: the frame inherits the page's
|
|
145
|
+
* policy, but the browser enforces that copy in — and dispatches the
|
|
146
|
+
* violation on — the FRAME's document, which is opaque-origin and cannot
|
|
147
|
+
* be listened to from outside. A frame-side reporter would need host
|
|
148
|
+
* script injected into untrusted view HTML, a trust-boundary change this
|
|
149
|
+
* primitive deliberately does not make; the `sandboxPageUrl` mount owns
|
|
150
|
+
* its own policy and reports nothing here by construction.
|
|
151
|
+
*/
|
|
152
|
+
cspOrigins?: ViewCspOrigins;
|
|
153
|
+
/**
|
|
154
|
+
* A CSP violation ABOUT this view was observed (see {@link cspOrigins}).
|
|
155
|
+
* Fires at most once per attachment, as soon as the violation lands —
|
|
156
|
+
* typically BEFORE the negotiation window lapses, since a blocked runtime
|
|
157
|
+
* never gets to negotiate. `<GuueyView>` folds it into the no-handshake
|
|
158
|
+
* label; a custom renderer shows/logs it as it likes.
|
|
159
|
+
*/
|
|
160
|
+
onCspDiagnosis?: (diagnosis: ViewCspDiagnosis) => void;
|
|
161
|
+
/** CSP-violation event source, injectable for tests. Default: `document`. */
|
|
162
|
+
cspEvents?: ViewCspEvents;
|
|
119
163
|
/**
|
|
120
164
|
* How long to wait for `ui/initialize` before declaring
|
|
121
165
|
* `"no-handshake"` (ms). `0` disables the timer. Default 8000 — a view
|
package/dist/view-host.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"view-host.d.ts","sourceRoot":"","sources":["../src/view-host.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,OAAO,
|
|
1
|
+
{"version":3,"file":"view-host.d.ts","sourceRoot":"","sources":["../src/view-host.ts"],"names":[],"mappings":"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"}
|
package/dist/view-host.js
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
* Seeded from ggui's console `surface-host.ts` (donated, guuey#186 audit);
|
|
28
28
|
* re-derived here against the pure machine + our own tests.
|
|
29
29
|
*/
|
|
30
|
-
import { initialViewHostState, resourceReadResponse, teardownMessage, toolCallResponse, viewHostElapsed, viewHostReceive, } from "./view-host-protocol.js";
|
|
30
|
+
import { diagnoseCspViolation, initialViewHostState, resourceReadResponse, teardownMessage, toolCallResponse, viewHostElapsed, viewHostReceive, } from "./view-host-protocol.js";
|
|
31
31
|
import { unavailableToolCallResult, } from "./action.js";
|
|
32
32
|
const DEFAULT_HOST_INFO = { name: "guuey-view-host", version: "1" };
|
|
33
33
|
const DEFAULT_NEGOTIATION_TIMEOUT_MS = 8000;
|
|
@@ -151,12 +151,43 @@ export function attachViewHost(frame, config = {}) {
|
|
|
151
151
|
return () => window.removeEventListener("message", domListener);
|
|
152
152
|
};
|
|
153
153
|
const unsubscribe = subscribe();
|
|
154
|
+
// The CSP tripwire (guuey#235): armed only when the caller declared the
|
|
155
|
+
// view's origins — with none, there is nothing to match and nothing is
|
|
156
|
+
// installed. It only ever ADDS a diagnosis; it never changes what the
|
|
157
|
+
// machine does. Once per attachment: the first violation about the view
|
|
158
|
+
// is the diagnosis (later ones are the same failure repeating).
|
|
159
|
+
const unsubscribeCsp = (() => {
|
|
160
|
+
const { cspOrigins, onCspDiagnosis } = config;
|
|
161
|
+
if (cspOrigins === undefined)
|
|
162
|
+
return () => { };
|
|
163
|
+
let reported = false;
|
|
164
|
+
const onViolation = (event) => {
|
|
165
|
+
if (reported)
|
|
166
|
+
return;
|
|
167
|
+
const diagnosis = diagnoseCspViolation(event, cspOrigins);
|
|
168
|
+
if (diagnosis === undefined)
|
|
169
|
+
return;
|
|
170
|
+
reported = true;
|
|
171
|
+
onCspDiagnosis?.(diagnosis);
|
|
172
|
+
};
|
|
173
|
+
const { cspEvents } = config;
|
|
174
|
+
if (cspEvents !== undefined) {
|
|
175
|
+
cspEvents.addEventListener("securitypolicyviolation", onViolation);
|
|
176
|
+
return () => cspEvents.removeEventListener("securitypolicyviolation", onViolation);
|
|
177
|
+
}
|
|
178
|
+
if (typeof document === "undefined")
|
|
179
|
+
return () => { }; // no embedding document — nothing reports there
|
|
180
|
+
const domListener = (event) => onViolation(event);
|
|
181
|
+
document.addEventListener("securitypolicyviolation", domListener);
|
|
182
|
+
return () => document.removeEventListener("securitypolicyviolation", domListener);
|
|
183
|
+
})();
|
|
154
184
|
const timeoutMs = config.negotiationTimeoutMs ?? DEFAULT_NEGOTIATION_TIMEOUT_MS;
|
|
155
185
|
const timer = timeoutMs > 0 ? setTimeout(() => setState(viewHostElapsed(state)), timeoutMs) : undefined;
|
|
156
186
|
return () => {
|
|
157
187
|
if (timer !== undefined)
|
|
158
188
|
clearTimeout(timer);
|
|
159
189
|
unsubscribe();
|
|
190
|
+
unsubscribeCsp();
|
|
160
191
|
cachedWindow?.postMessage(teardownMessage(), "*");
|
|
161
192
|
};
|
|
162
193
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@guuey/mcp-apps-host",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.2",
|
|
4
4
|
"description": "The MCP Apps (SEP-1865) Host role for guuey's chat surfaces — view-mount narrowing across UI channels, ui:// locator rehydration by resources/read, and the sandbox-trust channel contract. Vendor-neutral: any spec-following MCP App mounts through it.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
package/src/index.ts
CHANGED
|
@@ -72,6 +72,10 @@ export {
|
|
|
72
72
|
TOOLS_CALL_METHOD,
|
|
73
73
|
viewHostElapsed,
|
|
74
74
|
viewHostReceive,
|
|
75
|
+
diagnoseCspViolation,
|
|
76
|
+
type CspViolationLike,
|
|
77
|
+
type ViewCspDiagnosis,
|
|
78
|
+
type ViewCspOrigins,
|
|
75
79
|
type ViewHostBehavior,
|
|
76
80
|
type ViewHostEffect,
|
|
77
81
|
type ViewHostOutbound,
|
|
@@ -85,6 +89,7 @@ export {
|
|
|
85
89
|
attachViewHost,
|
|
86
90
|
viewDocumentHtml,
|
|
87
91
|
type AttachViewHostConfig,
|
|
92
|
+
type ViewCspEvents,
|
|
88
93
|
type ViewFrameLike,
|
|
89
94
|
type ViewHostEvents,
|
|
90
95
|
} from "./view-host.js";
|
package/src/react.tsx
CHANGED
|
@@ -35,11 +35,11 @@
|
|
|
35
35
|
import { useEffect, useMemo, useRef, useState, type CSSProperties, type ReactNode } from "react";
|
|
36
36
|
import { attachViewHost, viewDocumentHtml, type AttachViewHostConfig } from "./view-host.js";
|
|
37
37
|
import { attachSandboxPageDelivery } from "./sandbox-page.js";
|
|
38
|
-
import type { ViewHostPhase } from "./view-host-protocol.js";
|
|
38
|
+
import type { ViewCspDiagnosis, ViewHostPhase } from "./view-host-protocol.js";
|
|
39
39
|
import type { ResolvedViewMount } from "./card-mount.js";
|
|
40
40
|
|
|
41
41
|
export { attachViewHost, viewDocumentHtml } from "./view-host.js";
|
|
42
|
-
export type { AttachViewHostConfig, ViewFrameLike, ViewHostEvents } from "./view-host.js";
|
|
42
|
+
export type { AttachViewHostConfig, ViewCspEvents, ViewFrameLike, ViewHostEvents } from "./view-host.js";
|
|
43
43
|
export {
|
|
44
44
|
attachSandboxPageDelivery,
|
|
45
45
|
isSandboxProxyReady,
|
|
@@ -47,7 +47,7 @@ export {
|
|
|
47
47
|
SANDBOX_RESOURCE_READY_METHOD,
|
|
48
48
|
type SandboxPageDeliveryConfig,
|
|
49
49
|
} from "./sandbox-page.js";
|
|
50
|
-
export type { ViewHostPhase } from "./view-host-protocol.js";
|
|
50
|
+
export type { ViewCspDiagnosis, ViewCspOrigins, ViewHostPhase } from "./view-host-protocol.js";
|
|
51
51
|
export type { ResolvedViewMount, ViewMount, ViewMountChannel } from "./card-mount.js";
|
|
52
52
|
|
|
53
53
|
/** Accessible name for a mounted view when the caller has nothing better. */
|
|
@@ -63,6 +63,8 @@ export interface GuueyViewProps
|
|
|
63
63
|
| "onReadResource"
|
|
64
64
|
| "onSizeChanged"
|
|
65
65
|
| "negotiationTimeoutMs"
|
|
66
|
+
| "cspOrigins"
|
|
67
|
+
| "onCspDiagnosis"
|
|
66
68
|
> {
|
|
67
69
|
/** The resolved card to mount (see `toolResultViewMount`/`resolveViewMount`). */
|
|
68
70
|
mount: ResolvedViewMount;
|
|
@@ -119,9 +121,12 @@ export interface GuueyViewProps
|
|
|
119
121
|
* "render nothing". The default: a quiet "Negotiating with view…" line
|
|
120
122
|
* while `"negotiating"`; a labeled failure for `"no-handshake"` on the
|
|
121
123
|
* `"ggui"` channel; nothing once `"connected"` (the view owns its
|
|
122
|
-
* pixels) and nothing for a silent `"inline"` card.
|
|
124
|
+
* pixels) and nothing for a silent `"inline"` card. The second argument
|
|
125
|
+
* is the CSP diagnosis when the tripwire caught one (see
|
|
126
|
+
* {@link AttachViewHostConfig.cspOrigins}) — the default label folds it
|
|
127
|
+
* in; a custom renderer decides how to show it.
|
|
123
128
|
*/
|
|
124
|
-
renderStatus?: (phase: ViewHostPhase) => ReactNode;
|
|
129
|
+
renderStatus?: (phase: ViewHostPhase, diagnosis?: ViewCspDiagnosis) => ReactNode;
|
|
125
130
|
}
|
|
126
131
|
|
|
127
132
|
const statusLineStyle: CSSProperties = {
|
|
@@ -136,10 +141,25 @@ const statusLineStyle: CSSProperties = {
|
|
|
136
141
|
pointerEvents: "none",
|
|
137
142
|
};
|
|
138
143
|
|
|
139
|
-
function defaultStatus(
|
|
144
|
+
function defaultStatus(
|
|
145
|
+
phase: ViewHostPhase,
|
|
146
|
+
channel: ResolvedViewMount["channel"],
|
|
147
|
+
diagnosis: ViewCspDiagnosis | undefined,
|
|
148
|
+
): ReactNode {
|
|
140
149
|
if (phase === "negotiating") {
|
|
141
150
|
return <p style={statusLineStyle}>Negotiating with view…</p>;
|
|
142
151
|
}
|
|
152
|
+
// A CSP diagnosis (guuey#235) is the WHY behind a silent frame — on any
|
|
153
|
+
// channel: a blocked runtime bundle never gets to negotiate, so the
|
|
154
|
+
// tripwire's verdict outranks the channel heuristic below. Actionable
|
|
155
|
+
// over accurate-but-mute: name the blocked URI and the allowance.
|
|
156
|
+
if (phase === "no-handshake" && diagnosis !== undefined) {
|
|
157
|
+
return (
|
|
158
|
+
<p role="alert" style={{ ...statusLineStyle, opacity: 1, pointerEvents: "auto" }}>
|
|
159
|
+
{diagnosis.message}
|
|
160
|
+
</p>
|
|
161
|
+
);
|
|
162
|
+
}
|
|
143
163
|
if (phase === "no-handshake" && channel === "ggui") {
|
|
144
164
|
// A ggui shell negotiates unconditionally before painting, so silence
|
|
145
165
|
// here is a boot failure with no other author — label it (role=alert
|
|
@@ -173,6 +193,8 @@ export function GuueyView(props: GuueyViewProps): ReactNode {
|
|
|
173
193
|
} = props;
|
|
174
194
|
const frameRef = useRef<HTMLIFrameElement>(null);
|
|
175
195
|
const [phase, setPhase] = useState<ViewHostPhase>("negotiating");
|
|
196
|
+
// The CSP tripwire's verdict for THIS document, if any (guuey#235).
|
|
197
|
+
const [diagnosis, setDiagnosis] = useState<ViewCspDiagnosis | undefined>(undefined);
|
|
176
198
|
// The view's own size report, applied only under `autoResize`.
|
|
177
199
|
const [reportedHeight, setReportedHeight] = useState<number | undefined>(undefined);
|
|
178
200
|
const html = viewDocumentHtml(mount.resource);
|
|
@@ -208,6 +230,7 @@ export function GuueyView(props: GuueyViewProps): ReactNode {
|
|
|
208
230
|
// document boots fresh, and the previous negotiation's phase must not
|
|
209
231
|
// paper over it — nor must the previous document's reported size.
|
|
210
232
|
setPhase("negotiating");
|
|
233
|
+
setDiagnosis(undefined);
|
|
211
234
|
setReportedHeight(undefined);
|
|
212
235
|
const frame = frameRef.current;
|
|
213
236
|
if (frame === null || html === undefined) return;
|
|
@@ -220,6 +243,10 @@ export function GuueyView(props: GuueyViewProps): ReactNode {
|
|
|
220
243
|
setPhase(next);
|
|
221
244
|
latest.current.onPhaseChange?.(next);
|
|
222
245
|
},
|
|
246
|
+
onCspDiagnosis: (found) => {
|
|
247
|
+
setDiagnosis(found);
|
|
248
|
+
latest.current.hostConfig.onCspDiagnosis?.(found);
|
|
249
|
+
},
|
|
223
250
|
onSizeChanged: (size) => {
|
|
224
251
|
if (latest.current.autoResize === true && size.height !== undefined) {
|
|
225
252
|
setReportedHeight(size.height);
|
|
@@ -308,7 +335,9 @@ export function GuueyView(props: GuueyViewProps): ReactNode {
|
|
|
308
335
|
border: 0,
|
|
309
336
|
}}
|
|
310
337
|
/>
|
|
311
|
-
{renderStatus !== undefined
|
|
338
|
+
{renderStatus !== undefined
|
|
339
|
+
? renderStatus(phase, diagnosis)
|
|
340
|
+
: defaultStatus(phase, mount.channel, diagnosis)}
|
|
312
341
|
</div>
|
|
313
342
|
);
|
|
314
343
|
}
|
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
type McpUiHostCapabilities,
|
|
40
40
|
type McpUiHostContext,
|
|
41
41
|
type McpUiInitializeResult,
|
|
42
|
+
type McpUiResourceCsp,
|
|
42
43
|
} from "@modelcontextprotocol/ext-apps";
|
|
43
44
|
import type { McpToolStructuredContent } from "./action.js";
|
|
44
45
|
import type { McpResourceReadResult } from "./reader.js";
|
|
@@ -398,3 +399,104 @@ export function viewHostReceive(
|
|
|
398
399
|
export function viewHostElapsed(state: ViewHostState): ViewHostState {
|
|
399
400
|
return state.phase === "negotiating" ? { ...state, phase: "no-handshake" } : state;
|
|
400
401
|
}
|
|
402
|
+
|
|
403
|
+
// ─── CSP diagnosis (guuey#235) ────────────────────────────────────────────
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* The origins a view needs the EMBEDDER's page to allow — the spec's own
|
|
407
|
+
* per-resource CSP declaration (`McpUiResourceCsp`: `connectDomains` →
|
|
408
|
+
* `connect-src`, `resourceDomains` → `script-src`/`style-src`/…,
|
|
409
|
+
* `frameDomains` → `frame-src`). This is the honest filter for the CSP
|
|
410
|
+
* tripwire: a `securitypolicyviolation` whose `blockedURI` lands on one of
|
|
411
|
+
* these hosts is a violation ABOUT the view, not about anything else the
|
|
412
|
+
* page loads. Empty/absent → nothing to match, tripwire inert.
|
|
413
|
+
*/
|
|
414
|
+
export type ViewCspOrigins = McpUiResourceCsp;
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* What the embedder can act on when its own CSP blocked the view: the
|
|
418
|
+
* URI the browser refused, the directive that refused it, and the entry
|
|
419
|
+
* that would allow it (the blocked URI's origin — the smallest allowance
|
|
420
|
+
* that fixes exactly this). Rides beside `"no-handshake"` — the phase
|
|
421
|
+
* stays honest ("it never negotiated"); this is WHY.
|
|
422
|
+
*/
|
|
423
|
+
export interface ViewCspDiagnosis {
|
|
424
|
+
blockedUri: string;
|
|
425
|
+
violatedDirective: string;
|
|
426
|
+
/** The origin to add under `violatedDirective` — e.g. `https://assets.mcp.example`. */
|
|
427
|
+
suggestedEntry: string;
|
|
428
|
+
/** Operator-facing sentence, ready to label. */
|
|
429
|
+
message: string;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* The slice of a `SecurityPolicyViolationEvent` the tripwire reads —
|
|
434
|
+
* structural, so Node tests hand in plain objects (lib.dom's class is not
|
|
435
|
+
* constructible outside a browser).
|
|
436
|
+
*/
|
|
437
|
+
export interface CspViolationLike {
|
|
438
|
+
blockedURI: string;
|
|
439
|
+
/** e.g. `script-src-elem`, `connect-src`. */
|
|
440
|
+
violatedDirective: string;
|
|
441
|
+
/** The directive as the policy spelled it (`script-src` may govern `script-src-elem`). */
|
|
442
|
+
effectiveDirective?: string;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/** Every declared origin's HOST, wildcards (`https://*.x`) reduced to their suffix. */
|
|
446
|
+
function declaredHosts(origins: ViewCspOrigins): { host: string; wildcard: boolean }[] {
|
|
447
|
+
const out: { host: string; wildcard: boolean }[] = [];
|
|
448
|
+
for (const list of [origins.connectDomains, origins.resourceDomains, origins.frameDomains]) {
|
|
449
|
+
for (const entry of list ?? []) {
|
|
450
|
+
// `https://*.example.com` → wildcard suffix `.example.com`
|
|
451
|
+
const wildcard = /^[a-z]+:\/\/\*\./i.exec(entry);
|
|
452
|
+
if (wildcard) {
|
|
453
|
+
out.push({ host: entry.slice(wildcard[0].length - 1).toLowerCase(), wildcard: true });
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
try {
|
|
457
|
+
out.push({ host: new URL(entry).hostname.toLowerCase(), wildcard: false });
|
|
458
|
+
} catch {
|
|
459
|
+
// A malformed declaration is producer-side wire data; it simply
|
|
460
|
+
// never matches. The tripwire only ever ADDS a diagnosis, never
|
|
461
|
+
// blocks, so there is nothing to guard.
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
return out;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Pure: is this violation ABOUT the view (its blocked URI lands on a
|
|
470
|
+
* declared origin), and if so, what should the embedder add?
|
|
471
|
+
*
|
|
472
|
+
* `blockedURI` is a full URL for network/script blocks; the browser sends
|
|
473
|
+
* bare tokens (`eval`, `inline`, `data`) for policy-class blocks — those
|
|
474
|
+
* carry no host and never match a declared origin, which is right: a
|
|
475
|
+
* `script-src eval` report is not the view's (see guuey#236 for the zod
|
|
476
|
+
* probe that produces exactly one such report at boot).
|
|
477
|
+
*/
|
|
478
|
+
export function diagnoseCspViolation(
|
|
479
|
+
violation: CspViolationLike,
|
|
480
|
+
origins: ViewCspOrigins | undefined,
|
|
481
|
+
): ViewCspDiagnosis | undefined {
|
|
482
|
+
if (origins === undefined) return undefined;
|
|
483
|
+
let blocked: URL;
|
|
484
|
+
try {
|
|
485
|
+
blocked = new URL(violation.blockedURI);
|
|
486
|
+
} catch {
|
|
487
|
+
return undefined; // bare token (eval/inline/data/…) — not a host, not the view's
|
|
488
|
+
}
|
|
489
|
+
const host = blocked.hostname.toLowerCase();
|
|
490
|
+
const hit = declaredHosts(origins).some((d) =>
|
|
491
|
+
d.wildcard ? host.endsWith(d.host) : host === d.host,
|
|
492
|
+
);
|
|
493
|
+
if (!hit) return undefined;
|
|
494
|
+
const directive = violation.effectiveDirective || violation.violatedDirective;
|
|
495
|
+
const suggestedEntry = blocked.origin;
|
|
496
|
+
return {
|
|
497
|
+
blockedUri: violation.blockedURI,
|
|
498
|
+
violatedDirective: directive,
|
|
499
|
+
suggestedEntry,
|
|
500
|
+
message: `This page's Content-Security-Policy blocks ${violation.blockedURI} (${directive}) — the view cannot start. Add \`${directive} ${suggestedEntry}\` to the page's policy.`,
|
|
501
|
+
};
|
|
502
|
+
}
|
package/src/view-host.ts
CHANGED
|
@@ -28,12 +28,16 @@
|
|
|
28
28
|
* re-derived here against the pure machine + our own tests.
|
|
29
29
|
*/
|
|
30
30
|
import {
|
|
31
|
+
diagnoseCspViolation,
|
|
31
32
|
initialViewHostState,
|
|
32
33
|
resourceReadResponse,
|
|
33
34
|
teardownMessage,
|
|
34
35
|
toolCallResponse,
|
|
35
36
|
viewHostElapsed,
|
|
36
37
|
viewHostReceive,
|
|
38
|
+
type CspViolationLike,
|
|
39
|
+
type ViewCspDiagnosis,
|
|
40
|
+
type ViewCspOrigins,
|
|
37
41
|
type ViewHostBehavior,
|
|
38
42
|
type ViewHostOutbound,
|
|
39
43
|
type ViewHostPhase,
|
|
@@ -74,6 +78,16 @@ export interface ViewHostEvents {
|
|
|
74
78
|
): void;
|
|
75
79
|
}
|
|
76
80
|
|
|
81
|
+
/**
|
|
82
|
+
* Where CSP violations are observed — the EMBEDDING document (the frame's
|
|
83
|
+
* blocked loads report there, not inside the opaque frame). Structural like
|
|
84
|
+
* {@link ViewHostEvents}, injectable for tests. Default: `document`.
|
|
85
|
+
*/
|
|
86
|
+
export interface ViewCspEvents {
|
|
87
|
+
addEventListener(type: "securitypolicyviolation", listener: (event: CspViolationLike) => void): void;
|
|
88
|
+
removeEventListener(type: "securitypolicyviolation", listener: (event: CspViolationLike) => void): void;
|
|
89
|
+
}
|
|
90
|
+
|
|
77
91
|
export interface AttachViewHostConfig {
|
|
78
92
|
/**
|
|
79
93
|
* Capabilities to advertise in the initialize result. Default: `{}` —
|
|
@@ -130,6 +144,41 @@ export interface AttachViewHostConfig {
|
|
|
130
144
|
onSizeChanged?: (size: { width?: number; height?: number }) => void;
|
|
131
145
|
/** Observe phase transitions (see {@link ViewHostPhase}). */
|
|
132
146
|
onPhaseChange?: (phase: ViewHostPhase) => void;
|
|
147
|
+
/**
|
|
148
|
+
* The origins this view needs the embedding PAGE's CSP to allow — the
|
|
149
|
+
* spec's per-resource declaration (`McpUiResourceCsp`). Given, the host
|
|
150
|
+
* arms a CSP tripwire for the attachment's lifetime: a
|
|
151
|
+
* `securitypolicyviolation` on the EMBEDDING document whose blocked URI
|
|
152
|
+
* lands on one of these hosts is upgraded from a silent "never
|
|
153
|
+
* negotiated" into an actionable {@link ViewCspDiagnosis} (guuey#235).
|
|
154
|
+
* The phase itself stays `"no-handshake"` — honest about WHAT happened;
|
|
155
|
+
* the diagnosis says WHY. Absent → tripwire not installed, zero behavior
|
|
156
|
+
* change (the default for every existing caller).
|
|
157
|
+
*
|
|
158
|
+
* REACH (pinned by the browser leg, `e2e/tests/sdk/view-host.spec.ts`):
|
|
159
|
+
* the listener lives on the embedding document, so it sees violations
|
|
160
|
+
* the PAGE incurs on the view's origins — a runtime bundle the page
|
|
161
|
+
* loads at page level, a live channel the page opens on the view's
|
|
162
|
+
* behalf (the shape ggui's landing tripwire exercised). It does NOT see
|
|
163
|
+
* a `srcdoc` view's own blocked loads: the frame inherits the page's
|
|
164
|
+
* policy, but the browser enforces that copy in — and dispatches the
|
|
165
|
+
* violation on — the FRAME's document, which is opaque-origin and cannot
|
|
166
|
+
* be listened to from outside. A frame-side reporter would need host
|
|
167
|
+
* script injected into untrusted view HTML, a trust-boundary change this
|
|
168
|
+
* primitive deliberately does not make; the `sandboxPageUrl` mount owns
|
|
169
|
+
* its own policy and reports nothing here by construction.
|
|
170
|
+
*/
|
|
171
|
+
cspOrigins?: ViewCspOrigins;
|
|
172
|
+
/**
|
|
173
|
+
* A CSP violation ABOUT this view was observed (see {@link cspOrigins}).
|
|
174
|
+
* Fires at most once per attachment, as soon as the violation lands —
|
|
175
|
+
* typically BEFORE the negotiation window lapses, since a blocked runtime
|
|
176
|
+
* never gets to negotiate. `<GuueyView>` folds it into the no-handshake
|
|
177
|
+
* label; a custom renderer shows/logs it as it likes.
|
|
178
|
+
*/
|
|
179
|
+
onCspDiagnosis?: (diagnosis: ViewCspDiagnosis) => void;
|
|
180
|
+
/** CSP-violation event source, injectable for tests. Default: `document`. */
|
|
181
|
+
cspEvents?: ViewCspEvents;
|
|
133
182
|
/**
|
|
134
183
|
* How long to wait for `ui/initialize` before declaring
|
|
135
184
|
* `"no-handshake"` (ms). `0` disables the timer. Default 8000 — a view
|
|
@@ -270,6 +319,33 @@ export function attachViewHost(frame: ViewFrameLike, config: AttachViewHostConfi
|
|
|
270
319
|
};
|
|
271
320
|
const unsubscribe = subscribe();
|
|
272
321
|
|
|
322
|
+
// The CSP tripwire (guuey#235): armed only when the caller declared the
|
|
323
|
+
// view's origins — with none, there is nothing to match and nothing is
|
|
324
|
+
// installed. It only ever ADDS a diagnosis; it never changes what the
|
|
325
|
+
// machine does. Once per attachment: the first violation about the view
|
|
326
|
+
// is the diagnosis (later ones are the same failure repeating).
|
|
327
|
+
const unsubscribeCsp = ((): (() => void) => {
|
|
328
|
+
const { cspOrigins, onCspDiagnosis } = config;
|
|
329
|
+
if (cspOrigins === undefined) return () => {};
|
|
330
|
+
let reported = false;
|
|
331
|
+
const onViolation = (event: CspViolationLike): void => {
|
|
332
|
+
if (reported) return;
|
|
333
|
+
const diagnosis = diagnoseCspViolation(event, cspOrigins);
|
|
334
|
+
if (diagnosis === undefined) return;
|
|
335
|
+
reported = true;
|
|
336
|
+
onCspDiagnosis?.(diagnosis);
|
|
337
|
+
};
|
|
338
|
+
const { cspEvents } = config;
|
|
339
|
+
if (cspEvents !== undefined) {
|
|
340
|
+
cspEvents.addEventListener("securitypolicyviolation", onViolation);
|
|
341
|
+
return () => cspEvents.removeEventListener("securitypolicyviolation", onViolation);
|
|
342
|
+
}
|
|
343
|
+
if (typeof document === "undefined") return () => {}; // no embedding document — nothing reports there
|
|
344
|
+
const domListener = (event: SecurityPolicyViolationEvent): void => onViolation(event);
|
|
345
|
+
document.addEventListener("securitypolicyviolation", domListener);
|
|
346
|
+
return () => document.removeEventListener("securitypolicyviolation", domListener);
|
|
347
|
+
})();
|
|
348
|
+
|
|
273
349
|
const timeoutMs = config.negotiationTimeoutMs ?? DEFAULT_NEGOTIATION_TIMEOUT_MS;
|
|
274
350
|
const timer =
|
|
275
351
|
timeoutMs > 0 ? setTimeout(() => setState(viewHostElapsed(state)), timeoutMs) : undefined;
|
|
@@ -277,6 +353,7 @@ export function attachViewHost(frame: ViewFrameLike, config: AttachViewHostConfi
|
|
|
277
353
|
return () => {
|
|
278
354
|
if (timer !== undefined) clearTimeout(timer);
|
|
279
355
|
unsubscribe();
|
|
356
|
+
unsubscribeCsp();
|
|
280
357
|
cachedWindow?.postMessage(teardownMessage(), "*");
|
|
281
358
|
};
|
|
282
359
|
}
|