@guuey/mcp-apps-host 0.4.0 → 0.5.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/README.md +18 -0
- package/dist/action.d.ts +6 -0
- package/dist/action.d.ts.map +1 -1
- package/dist/action.js +11 -6
- package/dist/card-mount.d.ts +18 -0
- package/dist/card-mount.d.ts.map +1 -1
- package/dist/card-mount.js +25 -0
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -2
- package/dist/react.d.ts +94 -0
- package/dist/react.d.ts.map +1 -0
- package/dist/react.js +158 -0
- package/dist/sandbox-page.d.ts +75 -0
- package/dist/sandbox-page.d.ts.map +1 -0
- package/dist/sandbox-page.js +40 -0
- package/dist/view-host-protocol.d.ts +167 -0
- package/dist/view-host-protocol.d.ts.map +1 -0
- package/dist/view-host-protocol.js +168 -0
- package/dist/view-host.d.ts +119 -0
- package/dist/view-host.d.ts.map +1 -0
- package/dist/view-host.js +143 -0
- package/package.json +18 -1
- package/src/action.ts +11 -6
- package/src/card-mount.ts +27 -0
- package/src/index.ts +33 -0
- package/src/react.tsx +268 -0
- package/src/sandbox-page.ts +116 -0
- package/src/view-host-protocol.ts +299 -0
- package/src/view-host.ts +241 -0
package/README.md
CHANGED
|
@@ -27,6 +27,24 @@ role's client-side narrowing and mount contract:
|
|
|
27
27
|
this assembly over guuey's platform proxy.
|
|
28
28
|
- **Sandbox-trust channels** (`ViewMountChannel`): which sandbox host page a
|
|
29
29
|
payload may mount in, until per-resource declared-CSP construction lands.
|
|
30
|
+
- **The host itself** (`attachViewHost`, and `<GuueyView>` from
|
|
31
|
+
`@guuey/mcp-apps-host/react`): a spec-following view opens with the
|
|
32
|
+
`ui/initialize` App handshake and **blocks on it** — mount material alone
|
|
33
|
+
renders a blank frame forever. `attachViewHost(iframe, config)` answers
|
|
34
|
+
that handshake for any embedder (framework-agnostic, one call); the React
|
|
35
|
+
component additionally owns iframe creation, lifecycle, and the safe
|
|
36
|
+
sandbox default (`allow-scripts` **without** `allow-same-origin` — with
|
|
37
|
+
it, agent-generated HTML would run as your origin — plus
|
|
38
|
+
`allow="clipboard-write"` so generated copy buttons work). Configurable
|
|
39
|
+
where a host genuinely varies: `hostCapabilities` (default `{}` —
|
|
40
|
+
advertise only what you implement), an optional `tools/call` relay hook
|
|
41
|
+
(privilege boundary, default off; pair with `createMcpUiActionRelay`),
|
|
42
|
+
`hostContext`, and the negotiation timeout. While a frame negotiates, the
|
|
43
|
+
state is labeled (`negotiating` / `connected` / `no-handshake`), never a
|
|
44
|
+
blank page.
|
|
45
|
+
- **Resolved-only mount walk** (`resolveViewMount`): chain it off
|
|
46
|
+
`toolResultViewMount`/`snapshotViewMount` with your reader and render
|
|
47
|
+
only `ResolvedViewMount`s — the locator round-trip is folded in.
|
|
30
48
|
|
|
31
49
|
Vendor-neutral by principle: ggui's MCP Apps run on any spec-following host
|
|
32
50
|
(claude.ai, chatgpt.com, guuey) precisely because they follow the spec; this
|
package/dist/action.d.ts
CHANGED
|
@@ -57,6 +57,12 @@ export type McpToolCallResult = {
|
|
|
57
57
|
export declare const UI_ACTION_TOOLS: ReadonlySet<string>;
|
|
58
58
|
/** The in-band answer for anything the relay cannot (or will not) do. */
|
|
59
59
|
export declare const UI_ACTION_UNAVAILABLE_TEXT = "This action isn't available right now.";
|
|
60
|
+
/**
|
|
61
|
+
* The in-band `isError` result for an action that cannot be performed —
|
|
62
|
+
* the relay's own refusals use it, and `attachViewHost` posts it when an
|
|
63
|
+
* embedder-supplied relay hook rejects (the view is always answered).
|
|
64
|
+
*/
|
|
65
|
+
export declare function unavailableToolCallResult(): McpToolCallResult;
|
|
60
66
|
/**
|
|
61
67
|
* Narrow an untrusted `tools/call` answer to the arms the sandbox may see.
|
|
62
68
|
* Unknown content arms are DROPPED (never forwarded opaque); a value that
|
package/dist/action.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"action.d.ts","sourceRoot":"","sources":["../src/action.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,oFAAoF;AACpF,MAAM,MAAM,kBAAkB,GAC1B;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC9B;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GACjD;IACE,IAAI,EAAE,UAAU,CAAC;IACjB,QAAQ,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,CAC3C;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,GAChB;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,CACnB,CAAC;CACH,CAAC;AAEN;;;;GAIG;AACH,MAAM,MAAM,wBAAwB,GAAG;IACrC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB,CAAC;AAEF;;;;;GAKG;AACH,MAAM,MAAM,iBAAiB,GAAG;IAC9B,OAAO,EAAE,kBAAkB,EAAE,CAAC;IAC9B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,iBAAiB,CAAC,EAAE,wBAAwB,CAAC;CAC9C,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,eAAe,EAAE,WAAW,CAAC,MAAM,CAE9C,CAAC;AAEH,yEAAyE;AACzE,eAAO,MAAM,0BAA0B,2CACG,CAAC;
|
|
1
|
+
{"version":3,"file":"action.d.ts","sourceRoot":"","sources":["../src/action.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,oFAAoF;AACpF,MAAM,MAAM,kBAAkB,GAC1B;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC9B;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GACjD;IACE,IAAI,EAAE,UAAU,CAAC;IACjB,QAAQ,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,CAC3C;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,GAChB;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,CACnB,CAAC;CACH,CAAC;AAEN;;;;GAIG;AACH,MAAM,MAAM,wBAAwB,GAAG;IACrC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB,CAAC;AAEF;;;;;GAKG;AACH,MAAM,MAAM,iBAAiB,GAAG;IAC9B,OAAO,EAAE,kBAAkB,EAAE,CAAC;IAC9B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,iBAAiB,CAAC,EAAE,wBAAwB,CAAC;CAC9C,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,eAAe,EAAE,WAAW,CAAC,MAAM,CAE9C,CAAC;AAEH,yEAAyE;AACzE,eAAO,MAAM,0BAA0B,2CACG,CAAC;AAE3C;;;;GAIG;AACH,wBAAgB,yBAAyB,IAAI,iBAAiB,CAK7D;AAkCD;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,iBAAiB,GAAG,SAAS,CAgB9E;AAED,iFAAiF;AACjF,MAAM,WAAW,0BAA0B;IACzC;;;;;OAKG;IACH,QAAQ,EAAE,CACR,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,wBAAwB,GAAG,SAAS,KACvC,OAAO,CAAC,OAAO,CAAC,CAAC;CACvB;AAED,uEAAuE;AACvE,MAAM,WAAW,eAAe;IAC9B,yEAAyE;IACzE,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,wBAAwB,CAAC;CACtC;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,0BAA0B,GAC/B,CAAC,OAAO,EAAE,eAAe,KAAK,OAAO,CAAC,iBAAiB,CAAC,CAa1D"}
|
package/dist/action.js
CHANGED
|
@@ -21,7 +21,12 @@ export const UI_ACTION_TOOLS = new Set([
|
|
|
21
21
|
]);
|
|
22
22
|
/** The in-band answer for anything the relay cannot (or will not) do. */
|
|
23
23
|
export const UI_ACTION_UNAVAILABLE_TEXT = "This action isn't available right now.";
|
|
24
|
-
|
|
24
|
+
/**
|
|
25
|
+
* The in-band `isError` result for an action that cannot be performed —
|
|
26
|
+
* the relay's own refusals use it, and `attachViewHost` posts it when an
|
|
27
|
+
* embedder-supplied relay hook rejects (the view is always answered).
|
|
28
|
+
*/
|
|
29
|
+
export function unavailableToolCallResult() {
|
|
25
30
|
return {
|
|
26
31
|
content: [{ type: "text", text: UI_ACTION_UNAVAILABLE_TEXT }],
|
|
27
32
|
isError: true,
|
|
@@ -89,18 +94,18 @@ export function asToolCallResult(value) {
|
|
|
89
94
|
export function createMcpUiActionRelay(deps) {
|
|
90
95
|
return async (request) => {
|
|
91
96
|
if (!UI_ACTION_TOOLS.has(request.name))
|
|
92
|
-
return
|
|
97
|
+
return unavailableToolCallResult();
|
|
93
98
|
if (!request.resourceUri.startsWith("ui://"))
|
|
94
|
-
return
|
|
99
|
+
return unavailableToolCallResult();
|
|
95
100
|
let raw;
|
|
96
101
|
try {
|
|
97
102
|
raw = await deps.callTool(request.resourceUri, request.name, request.arguments);
|
|
98
103
|
}
|
|
99
104
|
catch {
|
|
100
|
-
return
|
|
105
|
+
return unavailableToolCallResult(); // transport failure == unavailable, in-band
|
|
101
106
|
}
|
|
102
107
|
if (raw === undefined)
|
|
103
|
-
return
|
|
104
|
-
return asToolCallResult(raw) ??
|
|
108
|
+
return unavailableToolCallResult();
|
|
109
|
+
return asToolCallResult(raw) ?? unavailableToolCallResult();
|
|
105
110
|
};
|
|
106
111
|
}
|
package/dist/card-mount.d.ts
CHANGED
|
@@ -97,4 +97,22 @@ export declare function toolResultViewMount(block: Extract<AgBlock, {
|
|
|
97
97
|
* the spec-consistent template fetch, vendor-neutral.
|
|
98
98
|
*/
|
|
99
99
|
export declare function snapshotViewMount(cardSnapshot: JsonValue): ViewMount | undefined;
|
|
100
|
+
/**
|
|
101
|
+
* The resolved-only convenience over the mount union (guuey#186 G6): every
|
|
102
|
+
* consumer that renders was writing the same two-call walk — narrow the
|
|
103
|
+
* union, then feed the `"locator"` arm to a reader. This collapses it:
|
|
104
|
+
*
|
|
105
|
+
* - already-resolved mounts pass through untouched (no reader round-trip);
|
|
106
|
+
* - a `"locator"` arm resolves via the reader — or the honest `undefined`
|
|
107
|
+
* (placeholder) when no reader is wired: never a stale mount;
|
|
108
|
+
* - a reader that answers with ANOTHER locator is treated as a miss. The
|
|
109
|
+
* {@link UiResourceReader} contract says a read yields mount material or
|
|
110
|
+
* nothing (guuey#127); a locator answer would loop, so the honest
|
|
111
|
+
* reading is "could not resolve", not recursion.
|
|
112
|
+
*
|
|
113
|
+
* Takes `ViewMount | undefined` so it chains directly off
|
|
114
|
+
* `toolResultViewMount`/`snapshotViewMount` without a narrowing dance at
|
|
115
|
+
* the call site.
|
|
116
|
+
*/
|
|
117
|
+
export declare function resolveViewMount(mount: ViewMount | undefined, reader?: UiResourceReader): Promise<ResolvedViewMount | undefined>;
|
|
100
118
|
//# sourceMappingURL=card-mount.d.ts.map
|
package/dist/card-mount.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"card-mount.d.ts","sourceRoot":"","sources":["../src/card-mount.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,OAAO,EAAuD,KAAK,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAE/G,OAAO,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAW/D,8EAA8E;AAC9E,MAAM,MAAM,gBAAgB,GAAG,QAAQ,GAAG,MAAM,GAAG,SAAS,CAAC;AAE7D;;;;;;;;;;;;;GAaG;AACH,MAAM,MAAM,SAAS,GAAG,iBAAiB,GAAG,gBAAgB,CAAC;AAE7D;;;;GAIG;AACH,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,QAAQ,GAAG,MAAM,CAAC;IAC3B,+EAA+E;IAC/E,QAAQ,EAAE,oBAAoB,CAAC;CAChC;AAED,6EAA6E;AAC7E,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,SAAS,CAAC;IACnB,uEAAuE;IACvE,WAAW,EAAE,MAAM,CAAC;CACrB;AAED;;;;;;;;GAQG;AACH,MAAM,MAAM,gBAAgB,GAAG,CAAC,WAAW,EAAE,MAAM,KAAK,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC,CAAC;AAEvF;;;;GAIG;AACH,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,OAAO,CAAC,OAAO,EAAE;IAAE,IAAI,EAAE,aAAa,CAAA;CAAE,CAAC,GAC/C,SAAS,GAAG,SAAS,CAkBvB;AAED;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAAC,YAAY,EAAE,SAAS,GAAG,SAAS,GAAG,SAAS,CAUhF"}
|
|
1
|
+
{"version":3,"file":"card-mount.d.ts","sourceRoot":"","sources":["../src/card-mount.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,OAAO,EAAuD,KAAK,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAE/G,OAAO,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAW/D,8EAA8E;AAC9E,MAAM,MAAM,gBAAgB,GAAG,QAAQ,GAAG,MAAM,GAAG,SAAS,CAAC;AAE7D;;;;;;;;;;;;;GAaG;AACH,MAAM,MAAM,SAAS,GAAG,iBAAiB,GAAG,gBAAgB,CAAC;AAE7D;;;;GAIG;AACH,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,QAAQ,GAAG,MAAM,CAAC;IAC3B,+EAA+E;IAC/E,QAAQ,EAAE,oBAAoB,CAAC;CAChC;AAED,6EAA6E;AAC7E,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,SAAS,CAAC;IACnB,uEAAuE;IACvE,WAAW,EAAE,MAAM,CAAC;CACrB;AAED;;;;;;;;GAQG;AACH,MAAM,MAAM,gBAAgB,GAAG,CAAC,WAAW,EAAE,MAAM,KAAK,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC,CAAC;AAEvF;;;;GAIG;AACH,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,OAAO,CAAC,OAAO,EAAE;IAAE,IAAI,EAAE,aAAa,CAAA;CAAE,CAAC,GAC/C,SAAS,GAAG,SAAS,CAkBvB;AAED;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAAC,YAAY,EAAE,SAAS,GAAG,SAAS,GAAG,SAAS,CAUhF;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,gBAAgB,CACpC,KAAK,EAAE,SAAS,GAAG,SAAS,EAC5B,MAAM,CAAC,EAAE,gBAAgB,GACxB,OAAO,CAAC,iBAAiB,GAAG,SAAS,CAAC,CAKxC"}
|
package/dist/card-mount.js
CHANGED
|
@@ -89,6 +89,31 @@ export function snapshotViewMount(cardSnapshot) {
|
|
|
89
89
|
}
|
|
90
90
|
return undefined;
|
|
91
91
|
}
|
|
92
|
+
/**
|
|
93
|
+
* The resolved-only convenience over the mount union (guuey#186 G6): every
|
|
94
|
+
* consumer that renders was writing the same two-call walk — narrow the
|
|
95
|
+
* union, then feed the `"locator"` arm to a reader. This collapses it:
|
|
96
|
+
*
|
|
97
|
+
* - already-resolved mounts pass through untouched (no reader round-trip);
|
|
98
|
+
* - a `"locator"` arm resolves via the reader — or the honest `undefined`
|
|
99
|
+
* (placeholder) when no reader is wired: never a stale mount;
|
|
100
|
+
* - a reader that answers with ANOTHER locator is treated as a miss. The
|
|
101
|
+
* {@link UiResourceReader} contract says a read yields mount material or
|
|
102
|
+
* nothing (guuey#127); a locator answer would loop, so the honest
|
|
103
|
+
* reading is "could not resolve", not recursion.
|
|
104
|
+
*
|
|
105
|
+
* Takes `ViewMount | undefined` so it chains directly off
|
|
106
|
+
* `toolResultViewMount`/`snapshotViewMount` without a narrowing dance at
|
|
107
|
+
* the call site.
|
|
108
|
+
*/
|
|
109
|
+
export async function resolveViewMount(mount, reader) {
|
|
110
|
+
if (mount === undefined || mount.channel !== "locator")
|
|
111
|
+
return mount;
|
|
112
|
+
if (reader === undefined)
|
|
113
|
+
return undefined;
|
|
114
|
+
const read = await reader(mount.resourceUri);
|
|
115
|
+
return read === undefined || read.channel === "locator" ? undefined : read;
|
|
116
|
+
}
|
|
92
117
|
/**
|
|
93
118
|
* The blocks to scan inside a card snapshot: the stored `AgArtifact`'s `parts`
|
|
94
119
|
* when present, then the snapshot root itself — exactly `snapshotUiResource`'s own
|
package/dist/index.d.ts
CHANGED
|
@@ -5,7 +5,10 @@
|
|
|
5
5
|
*/
|
|
6
6
|
export { asResourcePayload, asUiResource, blockUiResource, isJsonObject, resourceHtml, scanProviderRawForUiResource, snapshotUiResource, toolResultUiResource, uiLocator, type McpUiResourcePayload, } from "./block-ui.js";
|
|
7
7
|
export { asGguiRender, asGguiRenderBootstrap, blockGguiRender, gguiRenderResource, gguiShellHtml, toolResultGguiRender, GGUI_RENDER_META_KEY, type GguiRenderBootstrap, type GguiRenderDescriptor, type GguiShellHtmlOptions, } from "./ggui-render.js";
|
|
8
|
-
export { snapshotViewMount, toolResultViewMount, type LocatorViewMount, type ResolvedViewMount, type UiResourceReader, type ViewMount, type ViewMountChannel, } from "./card-mount.js";
|
|
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
|
-
export { asToolCallResult, createMcpUiActionRelay, UI_ACTION_TOOLS, UI_ACTION_UNAVAILABLE_TEXT, type CreateMcpUiActionRelayDeps, type McpToolCallContent, type McpToolCallResult, type McpToolStructuredContent, type UiActionRequest, } from "./action.js";
|
|
10
|
+
export { asToolCallResult, createMcpUiActionRelay, unavailableToolCallResult, UI_ACTION_TOOLS, UI_ACTION_UNAVAILABLE_TEXT, type CreateMcpUiActionRelayDeps, type McpToolCallContent, type McpToolCallResult, type McpToolStructuredContent, type UiActionRequest, } from "./action.js";
|
|
11
|
+
export { initializeResult, initialViewHostState, 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";
|
|
13
|
+
export { attachSandboxPageDelivery, isSandboxProxyReady, SANDBOX_PROXY_READY_METHOD, SANDBOX_RESOURCE_READY_METHOD, type SandboxPageDeliveryConfig, } from "./sandbox-page.js";
|
|
11
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,oBAAoB,EACpB,SAAS,EACT,KAAK,oBAAoB,GAC1B,MAAM,eAAe,CAAC;AACvB,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,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,eAAe,EACf,0BAA0B,EAC1B,KAAK,0BAA0B,EAC/B,KAAK,kBAAkB,EACvB,KAAK,iBAAiB,EACtB,KAAK,wBAAwB,EAC7B,KAAK,eAAe,GACrB,MAAM,aAAa,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,oBAAoB,EACpB,SAAS,EACT,KAAK,oBAAoB,GAC1B,MAAM,eAAe,CAAC;AACvB,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,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,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"}
|
package/dist/index.js
CHANGED
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
*/
|
|
6
6
|
export { asResourcePayload, asUiResource, blockUiResource, isJsonObject, resourceHtml, scanProviderRawForUiResource, snapshotUiResource, toolResultUiResource, uiLocator, } from "./block-ui.js";
|
|
7
7
|
export { asGguiRender, asGguiRenderBootstrap, blockGguiRender, gguiRenderResource, gguiShellHtml, toolResultGguiRender, GGUI_RENDER_META_KEY, } from "./ggui-render.js";
|
|
8
|
-
export { snapshotViewMount, toolResultViewMount, } from "./card-mount.js";
|
|
8
|
+
export { resolveViewMount, snapshotViewMount, toolResultViewMount, } from "./card-mount.js";
|
|
9
9
|
export { createMcpUiResourceReader, uiResourceChannel, } from "./reader.js";
|
|
10
|
-
export { asToolCallResult, createMcpUiActionRelay, UI_ACTION_TOOLS, UI_ACTION_UNAVAILABLE_TEXT, } from "./action.js";
|
|
10
|
+
export { asToolCallResult, createMcpUiActionRelay, unavailableToolCallResult, UI_ACTION_TOOLS, UI_ACTION_UNAVAILABLE_TEXT, } from "./action.js";
|
|
11
|
+
export { initializeResult, initialViewHostState, teardownMessage, toolCallResponse, TOOLS_CALL_METHOD, viewHostElapsed, viewHostReceive, } from "./view-host-protocol.js";
|
|
12
|
+
export { attachViewHost, viewDocumentHtml, } from "./view-host.js";
|
|
13
|
+
export { attachSandboxPageDelivery, isSandboxProxyReady, SANDBOX_PROXY_READY_METHOD, SANDBOX_RESOURCE_READY_METHOD, } from "./sandbox-page.js";
|
package/dist/react.d.ts
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React entry point (`@guuey/mcp-apps-host/react`).
|
|
3
|
+
*
|
|
4
|
+
* `<GuueyView>` is the one React-coupled surface — the root subpath stays
|
|
5
|
+
* React-free (narrowing, rehydration, the relay, `attachViewHost` itself),
|
|
6
|
+
* so server-side consumers never import React at all. The component is a
|
|
7
|
+
* CONVENIENCE composition of the framework-agnostic primitive: iframe
|
|
8
|
+
* creation + the sandbox invariant + the handshake + lifecycle. A host
|
|
9
|
+
* that needs a different composition (a transcript renderer, a non-React
|
|
10
|
+
* surface) uses `attachViewHost` directly.
|
|
11
|
+
*
|
|
12
|
+
* ## Sandbox posture (invariant, not preference)
|
|
13
|
+
*
|
|
14
|
+
* `sandbox="allow-scripts"` WITHOUT `allow-same-origin`: a `srcdoc` frame
|
|
15
|
+
* inherits its embedder's origin, so granting both would run
|
|
16
|
+
* agent-generated HTML AS the embedding page — reach into its DOM and its
|
|
17
|
+
* signed-in session, an XSS by construction. Dropping `allow-same-origin`
|
|
18
|
+
* puts the document in an opaque origin instead. Extra flags ride ON TOP
|
|
19
|
+
* via {@link GuueyViewProps.dangerouslyAddSandboxFlags} — named the way it
|
|
20
|
+
* is because every flag it adds widens what agent-generated HTML can do.
|
|
21
|
+
* `allow="clipboard-write"` is delegated by default: generated views own
|
|
22
|
+
* copy buttons, and without the delegation every one of them silently
|
|
23
|
+
* no-ops inside the opaque origin.
|
|
24
|
+
*
|
|
25
|
+
* ## Who paints which state
|
|
26
|
+
*
|
|
27
|
+
* While `"negotiating"`, the component shows a small non-blocking status
|
|
28
|
+
* line — never a bare blank frame (guuey#186 audit). On `"connected"` the
|
|
29
|
+
* view owns its pixels (and its own failures) and the line disappears. On
|
|
30
|
+
* `"no-handshake"` the meaning is channel-aware: a `"ggui"` shell always
|
|
31
|
+
* negotiates, so silence is a boot failure and is labeled as one; an
|
|
32
|
+
* `"inline"` card is arbitrary tenant HTML with no handshake obligation,
|
|
33
|
+
* so the status line simply retires and the document stands as rendered.
|
|
34
|
+
*/
|
|
35
|
+
import { type CSSProperties, type ReactNode } from "react";
|
|
36
|
+
import { type AttachViewHostConfig } from "./view-host.js";
|
|
37
|
+
import type { ViewHostPhase } from "./view-host-protocol.js";
|
|
38
|
+
import type { ResolvedViewMount } from "./card-mount.js";
|
|
39
|
+
export { attachViewHost, viewDocumentHtml } from "./view-host.js";
|
|
40
|
+
export type { AttachViewHostConfig, ViewFrameLike, ViewHostEvents } from "./view-host.js";
|
|
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";
|
|
43
|
+
export type { ResolvedViewMount, ViewMount, ViewMountChannel } from "./card-mount.js";
|
|
44
|
+
export interface GuueyViewProps extends Pick<AttachViewHostConfig, "hostCapabilities" | "hostInfo" | "hostContext" | "onCallTool" | "negotiationTimeoutMs"> {
|
|
45
|
+
/** The resolved card to mount (see `toolResultViewMount`/`resolveViewMount`). */
|
|
46
|
+
mount: ResolvedViewMount;
|
|
47
|
+
/**
|
|
48
|
+
* Opt into the TWO-ORIGIN mount: instead of `srcdoc`, the frame loads
|
|
49
|
+
* this host-served sandbox page (guuey's `/mcp-app-sandbox` pattern — the
|
|
50
|
+
* caller builds the full URL, channel/app query included) and the
|
|
51
|
+
* document is delivered over the page's relay protocol
|
|
52
|
+
* (`attachSandboxPageDelivery`). Why: a `srcdoc` frame INHERITS the
|
|
53
|
+
* embedder's CSP, so its egress confinement is whatever the page happens
|
|
54
|
+
* to carry; the sandbox page is served WITH the per-request CSP that
|
|
55
|
+
* confines the mount — and the untrusted document lands in the page's
|
|
56
|
+
* own inner opaque frame, never in this one. In this mode the frame's
|
|
57
|
+
* `sandbox` gains `allow-same-origin` — REQUIRED and safe: the frame
|
|
58
|
+
* holds the cross-origin RELAY PAGE (which must run as its real origin
|
|
59
|
+
* for its CSP + referrer checks to mean anything), never agent HTML.
|
|
60
|
+
* The page must be a genuinely different origin; a same-origin URL is
|
|
61
|
+
* refused with a labeled state, never mounted.
|
|
62
|
+
*/
|
|
63
|
+
sandboxPageUrl?: string;
|
|
64
|
+
/**
|
|
65
|
+
* Sandbox flags appended to the safe default (`allow-scripts`). Every
|
|
66
|
+
* entry widens what agent-generated HTML may do — `allow-same-origin`
|
|
67
|
+
* in particular hands it the embedder's origin. Prefer leaving unset.
|
|
68
|
+
* In `sandboxPageUrl` mode these forward to the INNER frame via the
|
|
69
|
+
* page's relay (which strips `allow-same-origin` regardless).
|
|
70
|
+
*/
|
|
71
|
+
dangerouslyAddSandboxFlags?: string[];
|
|
72
|
+
/** Permissions-Policy delegation for the frame. Default `clipboard-write`. */
|
|
73
|
+
allow?: string;
|
|
74
|
+
/** Accessible frame title. Default "Generated view". */
|
|
75
|
+
title?: string;
|
|
76
|
+
className?: string;
|
|
77
|
+
style?: CSSProperties;
|
|
78
|
+
/** Observe the negotiation phase (the same states the default UI labels). */
|
|
79
|
+
onPhaseChange?: (phase: ViewHostPhase) => void;
|
|
80
|
+
/**
|
|
81
|
+
* Replace the default status/failure line for a phase. Return `null` for
|
|
82
|
+
* "render nothing". The default: a quiet "Negotiating with view…" line
|
|
83
|
+
* while `"negotiating"`; a labeled failure for `"no-handshake"` on the
|
|
84
|
+
* `"ggui"` channel; nothing once `"connected"` (the view owns its
|
|
85
|
+
* pixels) and nothing for a silent `"inline"` card.
|
|
86
|
+
*/
|
|
87
|
+
renderStatus?: (phase: ViewHostPhase) => ReactNode;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Mount a resolved view and play the MCP Apps Host for it. See the module
|
|
91
|
+
* docblock for the sandbox and state contracts.
|
|
92
|
+
*/
|
|
93
|
+
export declare function GuueyView(props: GuueyViewProps): ReactNode;
|
|
94
|
+
//# sourceMappingURL=react.d.ts.map
|
|
@@ -0,0 +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;AAC7D,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,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAC1F,OAAO,EACL,yBAAyB,EACzB,mBAAmB,EACnB,0BAA0B,EAC1B,6BAA6B,EAC7B,KAAK,yBAAyB,GAC/B,MAAM,mBAAmB,CAAC;AAC3B,YAAY,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAC7D,YAAY,EAAE,iBAAiB,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAKtF,MAAM,WAAW,cACf,SAAQ,IAAI,CACV,oBAAoB,EACpB,kBAAkB,GAAG,UAAU,GAAG,aAAa,GAAG,YAAY,GAAG,sBAAsB,CACxF;IACD,iFAAiF;IACjF,KAAK,EAAE,iBAAiB,CAAC;IACzB;;;;;;;;;;;;;;;OAeG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;;;;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;;;;;;OAMG;IACH,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,SAAS,CAAC;CACpD;AA+BD;;;GAGG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,cAAc,GAAG,SAAS,CAiI1D"}
|
package/dist/react.js
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* React entry point (`@guuey/mcp-apps-host/react`).
|
|
4
|
+
*
|
|
5
|
+
* `<GuueyView>` is the one React-coupled surface — the root subpath stays
|
|
6
|
+
* React-free (narrowing, rehydration, the relay, `attachViewHost` itself),
|
|
7
|
+
* so server-side consumers never import React at all. The component is a
|
|
8
|
+
* CONVENIENCE composition of the framework-agnostic primitive: iframe
|
|
9
|
+
* creation + the sandbox invariant + the handshake + lifecycle. A host
|
|
10
|
+
* that needs a different composition (a transcript renderer, a non-React
|
|
11
|
+
* surface) uses `attachViewHost` directly.
|
|
12
|
+
*
|
|
13
|
+
* ## Sandbox posture (invariant, not preference)
|
|
14
|
+
*
|
|
15
|
+
* `sandbox="allow-scripts"` WITHOUT `allow-same-origin`: a `srcdoc` frame
|
|
16
|
+
* inherits its embedder's origin, so granting both would run
|
|
17
|
+
* agent-generated HTML AS the embedding page — reach into its DOM and its
|
|
18
|
+
* signed-in session, an XSS by construction. Dropping `allow-same-origin`
|
|
19
|
+
* puts the document in an opaque origin instead. Extra flags ride ON TOP
|
|
20
|
+
* via {@link GuueyViewProps.dangerouslyAddSandboxFlags} — named the way it
|
|
21
|
+
* is because every flag it adds widens what agent-generated HTML can do.
|
|
22
|
+
* `allow="clipboard-write"` is delegated by default: generated views own
|
|
23
|
+
* copy buttons, and without the delegation every one of them silently
|
|
24
|
+
* no-ops inside the opaque origin.
|
|
25
|
+
*
|
|
26
|
+
* ## Who paints which state
|
|
27
|
+
*
|
|
28
|
+
* While `"negotiating"`, the component shows a small non-blocking status
|
|
29
|
+
* line — never a bare blank frame (guuey#186 audit). On `"connected"` the
|
|
30
|
+
* view owns its pixels (and its own failures) and the line disappears. On
|
|
31
|
+
* `"no-handshake"` the meaning is channel-aware: a `"ggui"` shell always
|
|
32
|
+
* negotiates, so silence is a boot failure and is labeled as one; an
|
|
33
|
+
* `"inline"` card is arbitrary tenant HTML with no handshake obligation,
|
|
34
|
+
* so the status line simply retires and the document stands as rendered.
|
|
35
|
+
*/
|
|
36
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
37
|
+
import { attachViewHost, viewDocumentHtml } from "./view-host.js";
|
|
38
|
+
import { attachSandboxPageDelivery } from "./sandbox-page.js";
|
|
39
|
+
export { attachViewHost, viewDocumentHtml } from "./view-host.js";
|
|
40
|
+
export { attachSandboxPageDelivery, isSandboxProxyReady, SANDBOX_PROXY_READY_METHOD, SANDBOX_RESOURCE_READY_METHOD, } from "./sandbox-page.js";
|
|
41
|
+
/** Accessible name for a mounted view when the caller has nothing better. */
|
|
42
|
+
const DEFAULT_TITLE = "Generated view";
|
|
43
|
+
const statusLineStyle = {
|
|
44
|
+
position: "absolute",
|
|
45
|
+
insetInlineStart: 8,
|
|
46
|
+
insetBlockEnd: 8,
|
|
47
|
+
margin: 0,
|
|
48
|
+
padding: "2px 8px",
|
|
49
|
+
fontSize: 12,
|
|
50
|
+
lineHeight: "18px",
|
|
51
|
+
opacity: 0.65,
|
|
52
|
+
pointerEvents: "none",
|
|
53
|
+
};
|
|
54
|
+
function defaultStatus(phase, channel) {
|
|
55
|
+
if (phase === "negotiating") {
|
|
56
|
+
return _jsx("p", { style: statusLineStyle, children: "Negotiating with view\u2026" });
|
|
57
|
+
}
|
|
58
|
+
if (phase === "no-handshake" && channel === "ggui") {
|
|
59
|
+
// A ggui shell negotiates unconditionally before painting, so silence
|
|
60
|
+
// here is a boot failure with no other author — label it (role=alert
|
|
61
|
+
// so it is announced, not just drawn).
|
|
62
|
+
return (_jsx("p", { role: "alert", style: { ...statusLineStyle, opacity: 1, pointerEvents: "auto" }, children: "This view did not start \u2014 it never negotiated with the host." }));
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Mount a resolved view and play the MCP Apps Host for it. See the module
|
|
68
|
+
* docblock for the sandbox and state contracts.
|
|
69
|
+
*/
|
|
70
|
+
export function GuueyView(props) {
|
|
71
|
+
const { mount, sandboxPageUrl, dangerouslyAddSandboxFlags, allow, title, className, style, onPhaseChange, renderStatus, ...hostConfig } = props;
|
|
72
|
+
const frameRef = useRef(null);
|
|
73
|
+
const [phase, setPhase] = useState("negotiating");
|
|
74
|
+
const html = viewDocumentHtml(mount.resource);
|
|
75
|
+
// Vet the sandbox page once per URL. Same-origin is REFUSED (the widget's
|
|
76
|
+
// ResourceMount precedent, generalized): the whole point of the page is
|
|
77
|
+
// being a different origin — same-origin would hand the relay page (and
|
|
78
|
+
// through `allow-same-origin`, everything it can reach) the embedder's
|
|
79
|
+
// own origin.
|
|
80
|
+
const sandboxPage = useMemo(() => {
|
|
81
|
+
if (sandboxPageUrl === undefined)
|
|
82
|
+
return undefined;
|
|
83
|
+
let url;
|
|
84
|
+
try {
|
|
85
|
+
url = new URL(sandboxPageUrl);
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return "refused";
|
|
89
|
+
}
|
|
90
|
+
if (typeof window !== "undefined" && url.origin === window.location.origin)
|
|
91
|
+
return "refused";
|
|
92
|
+
return url;
|
|
93
|
+
}, [sandboxPageUrl]);
|
|
94
|
+
const page = sandboxPage instanceof URL ? sandboxPage : undefined;
|
|
95
|
+
// The attachment is keyed to the mounted DOCUMENT, not to every render's
|
|
96
|
+
// fresh callback identities — host config rides a ref so the effect's
|
|
97
|
+
// dependency list is honestly just the document identity.
|
|
98
|
+
const latest = useRef({ hostConfig, onPhaseChange, dangerouslyAddSandboxFlags });
|
|
99
|
+
latest.current = { hostConfig, onPhaseChange, dangerouslyAddSandboxFlags };
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
// Keyed to the same identity the frame is (the resource uri): a new
|
|
102
|
+
// document boots fresh, and the previous negotiation's phase must not
|
|
103
|
+
// paper over it.
|
|
104
|
+
setPhase("negotiating");
|
|
105
|
+
const frame = frameRef.current;
|
|
106
|
+
if (frame === null || html === undefined)
|
|
107
|
+
return;
|
|
108
|
+
if (sandboxPageUrl !== undefined && page === undefined)
|
|
109
|
+
return; // refused config — nothing mounts
|
|
110
|
+
const resourceUri = mount.resource.uri;
|
|
111
|
+
const detachHost = attachViewHost(frame, {
|
|
112
|
+
...latest.current.hostConfig,
|
|
113
|
+
resourceUri,
|
|
114
|
+
onPhaseChange: (next) => {
|
|
115
|
+
setPhase(next);
|
|
116
|
+
latest.current.onPhaseChange?.(next);
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
if (page === undefined)
|
|
120
|
+
return detachHost;
|
|
121
|
+
// Two-origin mode: the page announces readiness, the document is
|
|
122
|
+
// delivered over its relay (re-delivered on a reload's re-announce),
|
|
123
|
+
// and the view-host handshake crosses the same relay transparently.
|
|
124
|
+
const flags = latest.current.dangerouslyAddSandboxFlags;
|
|
125
|
+
const detachDelivery = attachSandboxPageDelivery(frame, {
|
|
126
|
+
pageOrigin: page.origin,
|
|
127
|
+
html,
|
|
128
|
+
...(flags !== undefined && flags.length > 0
|
|
129
|
+
? { sandbox: ["allow-scripts", ...flags].join(" ") }
|
|
130
|
+
: {}),
|
|
131
|
+
});
|
|
132
|
+
return () => {
|
|
133
|
+
detachDelivery();
|
|
134
|
+
detachHost();
|
|
135
|
+
};
|
|
136
|
+
}, [mount.resource.uri, html, sandboxPageUrl, page]);
|
|
137
|
+
if (html === undefined) {
|
|
138
|
+
// A resolved mount with no document is producer-side breakage; an
|
|
139
|
+
// empty frame would be a lie. Label it, in the same voice as the
|
|
140
|
+
// no-handshake state.
|
|
141
|
+
return (_jsx("div", { className: className, style: { position: "relative", ...style }, children: _jsx("p", { role: "alert", style: { ...statusLineStyle, opacity: 1, pointerEvents: "auto" }, children: "This view could not be displayed \u2014 its resource carries no document." }) }));
|
|
142
|
+
}
|
|
143
|
+
if (sandboxPageUrl !== undefined && page === undefined) {
|
|
144
|
+
// A malformed or SAME-ORIGIN sandbox page is a configuration state, not
|
|
145
|
+
// a property of the card — refused, labeled, never mounted.
|
|
146
|
+
return (_jsx("div", { className: className, style: { position: "relative", ...style }, children: _jsx("p", { role: "alert", style: { ...statusLineStyle, opacity: 1, pointerEvents: "auto" }, children: "Interactive view unavailable \u2014 the sandbox page is not usable from this origin." }) }));
|
|
147
|
+
}
|
|
148
|
+
return (_jsxs("div", { className: className, style: { position: "relative", ...style }, children: [_jsx("iframe", { ref: frameRef, ...(page !== undefined ? { src: page.href } : { srcDoc: html }), title: title ?? DEFAULT_TITLE,
|
|
149
|
+
// srcdoc mode: the INVARIANT — agent HTML in an opaque origin, extra
|
|
150
|
+
// flags only widen knowingly. Page mode: the frame holds the
|
|
151
|
+
// cross-origin RELAY PAGE, which must run as its real origin
|
|
152
|
+
// (`allow-same-origin`) for its CSP/referrer machinery to exist at
|
|
153
|
+
// all; the agent HTML lands in the page's own inner opaque frame,
|
|
154
|
+
// and the caller's extra flags travel to THAT frame via the relay.
|
|
155
|
+
sandbox: page !== undefined
|
|
156
|
+
? "allow-scripts allow-same-origin allow-forms"
|
|
157
|
+
: ["allow-scripts", ...(dangerouslyAddSandboxFlags ?? [])].join(" "), allow: allow ?? "clipboard-write", style: { display: "block", width: "100%", height: "100%", border: 0 } }, `${page?.href ?? "srcdoc"}::${mount.resource.uri}`), renderStatus !== undefined ? renderStatus(phase) : defaultStatus(phase, mount.channel)] }));
|
|
158
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sandbox-PAGE document delivery — the client half of the two-origin mount
|
|
3
|
+
* (guuey#135 wave-3c, from the #186/#135 dogfood's finding 2).
|
|
4
|
+
*
|
|
5
|
+
* ## Why a second mount mode exists
|
|
6
|
+
*
|
|
7
|
+
* The default `<GuueyView>` mount is a `srcdoc` frame: zero configuration,
|
|
8
|
+
* opaque origin, correct sandbox posture — but a `srcdoc` document INHERITS
|
|
9
|
+
* the embedding page's Content-Security-Policy, so the strongest egress
|
|
10
|
+
* confinement it can have is whatever the embedder's page happens to carry.
|
|
11
|
+
* Guuey's production surfaces confine harder: the untrusted document mounts
|
|
12
|
+
* inside a HOST-SERVED sandbox page on a second origin, whose per-request
|
|
13
|
+
* CSP names exactly the egress that mount is entitled to (see the platform's
|
|
14
|
+
* `/mcp-app-sandbox` route — per-channel `connect-src`, per-app
|
|
15
|
+
* `frame-ancestors`). This module speaks that page's delivery protocol so
|
|
16
|
+
* any kit consumer can opt into the same confinement.
|
|
17
|
+
*
|
|
18
|
+
* ## The protocol (co-owned in-repo; two notifications)
|
|
19
|
+
*
|
|
20
|
+
* The page is the reference sandbox relay (adapted from the MCP ext-apps
|
|
21
|
+
* `basic-host` example, vendored at
|
|
22
|
+
* `create-agentic-app/templates-src/base/web/sandbox-proxy.ts` and served by
|
|
23
|
+
* the platform's landing route). Its wire is exactly two JSON-RPC
|
|
24
|
+
* notifications:
|
|
25
|
+
*
|
|
26
|
+
* 1. page → host: `ui/notifications/sandbox-proxy-ready` — the relay booted
|
|
27
|
+
* and is listening;
|
|
28
|
+
* 2. host → page: `ui/notifications/sandbox-resource-ready` with
|
|
29
|
+
* `params.html` (+ optional `params.sandbox` tokens for the INNER frame —
|
|
30
|
+
* the page strips `allow-same-origin` from any value regardless).
|
|
31
|
+
*
|
|
32
|
+
* Every other message crosses the page transparently in both directions,
|
|
33
|
+
* which is why `attachViewHost` works unchanged on top of this delivery: the
|
|
34
|
+
* view's `ui/initialize` arrives relayed with `event.source` still the OUTER
|
|
35
|
+
* frame's window, and the host's answers relay inward.
|
|
36
|
+
*
|
|
37
|
+
* ## Identity + targeting
|
|
38
|
+
*
|
|
39
|
+
* Inbound messages are matched by `event.source === frame.contentWindow` —
|
|
40
|
+
* the package's standing identity invariant (`view-host.ts`). Outbound
|
|
41
|
+
* delivery targets `config.pageOrigin` EXPLICITLY (never `'*'`): unlike a
|
|
42
|
+
* srcdoc view, the sandbox page has a real origin, and the document being
|
|
43
|
+
* delivered is agent-generated content the caller confined on purpose — if
|
|
44
|
+
* the frame somehow navigated elsewhere, the browser drops the message
|
|
45
|
+
* instead of handing the document to the wrong receiver.
|
|
46
|
+
*/
|
|
47
|
+
import type { ViewFrameLike, ViewHostEvents } from "./view-host.js";
|
|
48
|
+
export declare const SANDBOX_PROXY_READY_METHOD = "ui/notifications/sandbox-proxy-ready";
|
|
49
|
+
export declare const SANDBOX_RESOURCE_READY_METHOD = "ui/notifications/sandbox-resource-ready";
|
|
50
|
+
/** Structural check for the page's ready notification. */
|
|
51
|
+
export declare function isSandboxProxyReady(data: unknown): boolean;
|
|
52
|
+
export interface SandboxPageDeliveryConfig {
|
|
53
|
+
/**
|
|
54
|
+
* The sandbox page's origin — the ONLY target the document is posted to.
|
|
55
|
+
* Derive it from the page URL the frame was given (`new URL(url).origin`).
|
|
56
|
+
*/
|
|
57
|
+
pageOrigin: string;
|
|
58
|
+
/** The document to deliver (the view's `viewDocumentHtml`). */
|
|
59
|
+
html: string;
|
|
60
|
+
/**
|
|
61
|
+
* Inner-frame sandbox tokens forwarded as `params.sandbox`. The page's
|
|
62
|
+
* `safeSandbox` strips `allow-same-origin` and guarantees `allow-scripts`
|
|
63
|
+
* whatever is sent — this only ever WIDENS within the page's own bounds.
|
|
64
|
+
*/
|
|
65
|
+
sandbox?: string;
|
|
66
|
+
/** Message-event source, injectable for tests. Default: `window`. */
|
|
67
|
+
events?: ViewHostEvents;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Deliver a view document to a mounted sandbox page, re-delivering on every
|
|
71
|
+
* `sandbox-proxy-ready` (a reloaded page announces again and must be
|
|
72
|
+
* re-seeded). Returns a detach function.
|
|
73
|
+
*/
|
|
74
|
+
export declare function attachSandboxPageDelivery(frame: ViewFrameLike, config: SandboxPageDeliveryConfig): () => void;
|
|
75
|
+
//# sourceMappingURL=sandbox-page.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sandbox-page.d.ts","sourceRoot":"","sources":["../src/sandbox-page.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AACH,OAAO,KAAK,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAEpE,eAAO,MAAM,0BAA0B,yCAAyC,CAAC;AACjF,eAAO,MAAM,6BAA6B,4CAA4C,CAAC;AAEvF,0DAA0D;AAC1D,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAO1D;AAED,MAAM,WAAW,yBAAyB;IACxC;;;OAGG;IACH,UAAU,EAAE,MAAM,CAAC;IACnB,+DAA+D;IAC/D,IAAI,EAAE,MAAM,CAAC;IACb;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,qEAAqE;IACrE,MAAM,CAAC,EAAE,cAAc,CAAC;CACzB;AAED;;;;GAIG;AACH,wBAAgB,yBAAyB,CACvC,KAAK,EAAE,aAAa,EACpB,MAAM,EAAE,yBAAyB,GAChC,MAAM,IAAI,CA4BZ"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export const SANDBOX_PROXY_READY_METHOD = "ui/notifications/sandbox-proxy-ready";
|
|
2
|
+
export const SANDBOX_RESOURCE_READY_METHOD = "ui/notifications/sandbox-resource-ready";
|
|
3
|
+
/** Structural check for the page's ready notification. */
|
|
4
|
+
export function isSandboxProxyReady(data) {
|
|
5
|
+
return (typeof data === "object" &&
|
|
6
|
+
data !== null &&
|
|
7
|
+
!Array.isArray(data) &&
|
|
8
|
+
data.method === SANDBOX_PROXY_READY_METHOD);
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Deliver a view document to a mounted sandbox page, re-delivering on every
|
|
12
|
+
* `sandbox-proxy-ready` (a reloaded page announces again and must be
|
|
13
|
+
* re-seeded). Returns a detach function.
|
|
14
|
+
*/
|
|
15
|
+
export function attachSandboxPageDelivery(frame, config) {
|
|
16
|
+
const deliver = () => {
|
|
17
|
+
frame.contentWindow?.postMessage({
|
|
18
|
+
jsonrpc: "2.0",
|
|
19
|
+
method: SANDBOX_RESOURCE_READY_METHOD,
|
|
20
|
+
params: {
|
|
21
|
+
html: config.html,
|
|
22
|
+
...(config.sandbox !== undefined ? { sandbox: config.sandbox } : {}),
|
|
23
|
+
},
|
|
24
|
+
}, config.pageOrigin);
|
|
25
|
+
};
|
|
26
|
+
const onMessage = (event) => {
|
|
27
|
+
if (frame.contentWindow === null || event.source !== frame.contentWindow)
|
|
28
|
+
return;
|
|
29
|
+
if (isSandboxProxyReady(event.data))
|
|
30
|
+
deliver();
|
|
31
|
+
};
|
|
32
|
+
const { events } = config;
|
|
33
|
+
if (events !== undefined) {
|
|
34
|
+
events.addEventListener("message", onMessage);
|
|
35
|
+
return () => events.removeEventListener("message", onMessage);
|
|
36
|
+
}
|
|
37
|
+
const domListener = (event) => onMessage(event);
|
|
38
|
+
window.addEventListener("message", domListener);
|
|
39
|
+
return () => window.removeEventListener("message", domListener);
|
|
40
|
+
}
|