@assistant-ui/react 0.15.25 → 0.15.26
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/hooks/useToolCallElapsed.d.ts.map +1 -1
- package/dist/hooks/useToolCallElapsed.js +3 -2
- package/dist/hooks/useToolCallElapsed.js.map +1 -1
- package/dist/index.d.ts +30 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/mcp-apps/McpAppRenderer.d.ts.map +1 -1
- package/dist/mcp-apps/McpAppRenderer.js +3 -2
- package/dist/mcp-apps/McpAppRenderer.js.map +1 -1
- package/dist/mcp-apps/app-frame.d.ts.map +1 -1
- package/dist/mcp-apps/app-frame.js +56 -46
- package/dist/mcp-apps/app-frame.js.map +1 -1
- package/dist/mcp-apps/csp.d.ts +4 -0
- package/dist/mcp-apps/csp.d.ts.map +1 -0
- package/dist/mcp-apps/csp.js +93 -0
- package/dist/mcp-apps/csp.js.map +1 -0
- package/dist/mcp-apps/types.d.ts +1 -0
- package/dist/mcp-apps/types.d.ts.map +1 -1
- package/dist/mcp-apps/types.js.map +1 -1
- package/dist/primitives/assistantModal/AssistantModalRoot.d.ts +1 -0
- package/dist/primitives/assistantModal/AssistantModalRoot.d.ts.map +1 -1
- package/dist/primitives/assistantModal/AssistantModalRoot.js.map +1 -1
- package/dist/primitives/attachment.d.ts +3 -1
- package/dist/primitives/attachment.d.ts.map +1 -1
- package/dist/primitives/attachment.js.map +1 -1
- package/dist/primitives/composer/ComposerCancel.d.ts +5 -1
- package/dist/primitives/composer/ComposerCancel.d.ts.map +1 -1
- package/dist/primitives/composer/ComposerCancel.js +14 -1
- package/dist/primitives/composer/ComposerCancel.js.map +1 -1
- package/dist/primitives/composer/ComposerInput.d.ts +14 -0
- package/dist/primitives/composer/ComposerInput.d.ts.map +1 -1
- package/dist/primitives/composer/ComposerInput.js +4 -3
- package/dist/primitives/composer/ComposerInput.js.map +1 -1
- package/dist/primitives/composer/ComposerRoot.d.ts.map +1 -1
- package/dist/primitives/composer/ComposerRoot.js +24 -19
- package/dist/primitives/composer/ComposerRoot.js.map +1 -1
- package/dist/primitives/composer/composerInputSelector.d.ts +3 -0
- package/dist/primitives/composer/composerInputSelector.d.ts.map +1 -0
- package/dist/primitives/composer/composerInputSelector.js +7 -0
- package/dist/primitives/composer/composerInputSelector.js.map +1 -0
- package/dist/primitives/composer/useComposerCancelWithFocus.d.ts +2 -0
- package/dist/primitives/composer/useComposerCancelWithFocus.d.ts.map +1 -0
- package/dist/primitives/composer/useComposerCancelWithFocus.js +42 -0
- package/dist/primitives/composer/useComposerCancelWithFocus.js.map +1 -0
- package/dist/primitives/composer.d.ts +35 -14
- package/dist/primitives/composer.d.ts.map +1 -1
- package/dist/primitives/composer.js.map +1 -1
- package/dist/primitives/message/MessagePartsGrouped.d.ts +8 -1
- package/dist/primitives/message/MessagePartsGrouped.d.ts.map +1 -1
- package/dist/primitives/message/MessagePartsGrouped.js +58 -278
- package/dist/primitives/message/MessagePartsGrouped.js.map +1 -1
- package/dist/primitives/selectionToolbar/SelectionToolbarRoot.d.ts.map +1 -1
- package/dist/primitives/selectionToolbar/SelectionToolbarRoot.js +5 -2
- package/dist/primitives/selectionToolbar/SelectionToolbarRoot.js.map +1 -1
- package/dist/primitives/thread/ThreadViewport.d.ts.map +1 -1
- package/dist/primitives/thread/ThreadViewport.js +4 -3
- package/dist/primitives/thread/ThreadViewport.js.map +1 -1
- package/dist/primitives/thread/topAnchor/useTopAnchorReserve.js +2 -2
- package/dist/primitives/thread/topAnchor/useTopAnchorReserve.js.map +1 -1
- package/dist/primitives/thread/useThreadViewportAutoScroll.d.ts.map +1 -1
- package/dist/primitives/thread/useThreadViewportAutoScroll.js +90 -21
- package/dist/primitives/thread/useThreadViewportAutoScroll.js.map +1 -1
- package/dist/sandbox-host/SandboxHost.d.ts.map +1 -1
- package/dist/sandbox-host/SandboxHost.js +3 -2
- package/dist/sandbox-host/SandboxHost.js.map +1 -1
- package/dist/unstable/useComposerInput.d.ts +7 -4
- package/dist/unstable/useComposerInput.d.ts.map +1 -1
- package/dist/unstable/useComposerInput.js +4 -4
- package/dist/unstable/useComposerInput.js.map +1 -1
- package/dist/unstable/useComposerInputHistory.d.ts +3 -2
- package/dist/unstable/useComposerInputHistory.d.ts.map +1 -1
- package/dist/unstable/useComposerInputHistory.js +2 -2
- package/dist/unstable/useComposerInputHistory.js.map +1 -1
- package/dist/unstable/useLiveCompletionAdapter.d.ts +3 -2
- package/dist/unstable/useLiveCompletionAdapter.d.ts.map +1 -1
- package/dist/unstable/useLiveCompletionAdapter.js +8 -7
- package/dist/unstable/useLiveCompletionAdapter.js.map +1 -1
- package/dist/unstable/useMentionAdapter.d.ts +12 -3
- package/dist/unstable/useMentionAdapter.d.ts.map +1 -1
- package/dist/unstable/useMentionAdapter.js +2 -2
- package/dist/unstable/useMentionAdapter.js.map +1 -1
- package/dist/unstable/useMessageStallDetection.d.ts +4 -2
- package/dist/unstable/useMessageStallDetection.d.ts.map +1 -1
- package/dist/unstable/useMessageStallDetection.js +2 -2
- package/dist/unstable/useMessageStallDetection.js.map +1 -1
- package/dist/unstable/useSlashCommandAdapter.d.ts +5 -2
- package/dist/unstable/useSlashCommandAdapter.d.ts.map +1 -1
- package/dist/unstable/useSlashCommandAdapter.js +5 -4
- package/dist/unstable/useSlashCommandAdapter.js.map +1 -1
- package/dist/unstable/webmcp/useWebMcpProvider.d.ts +4 -0
- package/dist/unstable/webmcp/useWebMcpProvider.d.ts.map +1 -1
- package/dist/unstable/webmcp/useWebMcpProvider.js +2 -0
- package/dist/unstable/webmcp/useWebMcpProvider.js.map +1 -1
- package/dist/utils/getSelectionMessageId.d.ts.map +1 -1
- package/dist/utils/getSelectionMessageId.js +31 -8
- package/dist/utils/getSelectionMessageId.js.map +1 -1
- package/dist/utils/hooks/useOnResizeContent.d.ts.map +1 -1
- package/dist/utils/hooks/useOnResizeContent.js +2 -22
- package/dist/utils/hooks/useOnResizeContent.js.map +1 -1
- package/dist/utils/useIsomorphicLayoutEffect.d.ts +3 -0
- package/dist/utils/useIsomorphicLayoutEffect.d.ts.map +1 -0
- package/dist/utils/useIsomorphicLayoutEffect.js +6 -0
- package/dist/utils/useIsomorphicLayoutEffect.js.map +1 -0
- package/package.json +10 -10
- package/src/hooks/useToolCallElapsed.test.tsx +4 -2
- package/src/hooks/useToolCallElapsed.ts +3 -2
- package/src/index.ts +19 -2
- package/src/mcp-apps/McpAppRenderer.tsx +3 -9
- package/src/mcp-apps/app-frame.test.tsx +38 -0
- package/src/mcp-apps/app-frame.tsx +5 -8
- package/src/mcp-apps/csp.test.ts +112 -0
- package/src/mcp-apps/csp.ts +124 -0
- package/src/mcp-apps/types.ts +1 -0
- package/src/model-context/frame/AssistantFrame.test.ts +45 -0
- package/src/model-context/frame/SPEC_AssistantFrame.md +1 -0
- package/src/primitives/assistantModal/AssistantModalRoot.tsx +1 -0
- package/src/primitives/attachment.ts +4 -1
- package/src/primitives/composer/ComposerCancel.focus.test.tsx +307 -0
- package/src/primitives/composer/ComposerCancel.ts +8 -1
- package/src/primitives/composer/ComposerInput.tsx +19 -1
- package/src/primitives/composer/ComposerRoot.tsx +6 -5
- package/src/primitives/composer/composerInputSelector.ts +4 -0
- package/src/primitives/composer/useComposerCancelWithFocus.ts +57 -0
- package/src/primitives/composer.ts +56 -21
- package/src/primitives/message/MessagePartsGrouped.test.tsx +106 -2
- package/src/primitives/message/MessagePartsGrouped.tsx +36 -134
- package/src/primitives/selectionToolbar/SelectionToolbarRoot.test.tsx +20 -12
- package/src/primitives/selectionToolbar/SelectionToolbarRoot.tsx +6 -2
- package/src/primitives/thread/ThreadViewport.tsx +3 -3
- package/src/primitives/thread/topAnchor/useTopAnchorReserve.ts +2 -2
- package/src/primitives/thread/useThreadViewportAutoScroll.test.tsx +666 -63
- package/src/primitives/thread/useThreadViewportAutoScroll.ts +201 -73
- package/src/sandbox-host/SandboxHost.tsx +3 -8
- package/src/unstable/useComposerInput.ts +7 -4
- package/src/unstable/useComposerInputHistory.ts +3 -2
- package/src/unstable/useLiveCompletionAdapter.ts +9 -14
- package/src/unstable/useMentionAdapter.ts +12 -3
- package/src/unstable/useMessageStallDetection.ts +4 -2
- package/src/unstable/useSlashCommandAdapter.ts +8 -4
- package/src/unstable/webmcp/useWebMcpProvider.ts +4 -0
- package/src/utils/getSelectionMessageId.test.ts +231 -0
- package/src/utils/getSelectionMessageId.ts +45 -7
- package/src/utils/hooks/useOnResizeContent.ts +2 -29
- package/src/utils/useIsomorphicLayoutEffect.test.tsx +21 -0
- package/src/utils/useIsomorphicLayoutEffect.ts +9 -0
package/src/index.ts
CHANGED
|
@@ -164,11 +164,17 @@ export { useExternalStoreRuntime } from "./legacy-runtime/runtime-cores/external
|
|
|
164
164
|
export { useExternalStoreSharedOptions } from "@assistant-ui/core/react";
|
|
165
165
|
export {
|
|
166
166
|
useExternalMessageConverter,
|
|
167
|
+
/** @deprecated Experimental since 2025-01-26. Not scheduled for removal; the API may change in any release. */
|
|
167
168
|
convertExternalMessages as unstable_convertExternalMessages,
|
|
169
|
+
/** @deprecated Experimental since 2026-09-15. Not scheduled for removal; the API may change in any release. */
|
|
168
170
|
createExternalMessageConversionCache as unstable_createExternalMessageConversionCache,
|
|
171
|
+
/** @deprecated Experimental since 2026-09-15. Not scheduled for removal; the API may change in any release. */
|
|
169
172
|
type ExternalMessageConversionCache as Unstable_ExternalMessageConversionCache,
|
|
170
173
|
} from "./legacy-runtime/runtime-cores/external-store/external-message-converter";
|
|
171
|
-
export {
|
|
174
|
+
export {
|
|
175
|
+
/** @deprecated Experimental since 2025-02-02. Not scheduled for removal; the API may change in any release. */
|
|
176
|
+
createMessageConverter as unstable_createMessageConverter,
|
|
177
|
+
} from "./legacy-runtime/runtime-cores/external-store/createMessageConverter";
|
|
172
178
|
|
|
173
179
|
// --- local ---
|
|
174
180
|
export type {
|
|
@@ -496,15 +502,23 @@ export type { ToolExecutionStatus } from "./internal";
|
|
|
496
502
|
|
|
497
503
|
// Unstable - trigger popover (unified root for @ mentions, / slash commands, etc.)
|
|
498
504
|
export {
|
|
505
|
+
/** @deprecated Experimental since 2026-04-15. Not scheduled for removal; the API may change in any release. */
|
|
499
506
|
useTriggerPopoverRootContext as unstable_useTriggerPopoverRootContext,
|
|
507
|
+
/** @deprecated Experimental since 2026-04-15. Not scheduled for removal; the API may change in any release. */
|
|
500
508
|
useTriggerPopoverRootContextOptional as unstable_useTriggerPopoverRootContextOptional,
|
|
509
|
+
/** @deprecated Experimental since 2026-04-15. Not scheduled for removal; the API may change in any release. */
|
|
501
510
|
useTriggerPopoverScopeContext as unstable_useTriggerPopoverScopeContext,
|
|
511
|
+
/** @deprecated Experimental since 2026-04-15. Not scheduled for removal; the API may change in any release. */
|
|
502
512
|
useTriggerPopoverScopeContextOptional as unstable_useTriggerPopoverScopeContextOptional,
|
|
513
|
+
/** @deprecated Experimental since 2026-04-15. Not scheduled for removal; the API may change in any release. */
|
|
503
514
|
useTriggerPopoverTriggers as unstable_useTriggerPopoverTriggers,
|
|
515
|
+
/** @deprecated Experimental since 2026-04-15. Not scheduled for removal; the API may change in any release. */
|
|
504
516
|
useTriggerPopoverTriggersOptional as unstable_useTriggerPopoverTriggersOptional,
|
|
517
|
+
/** @deprecated Experimental since 2026-04-15. Not scheduled for removal; the API may change in any release. */
|
|
505
518
|
type RegisteredTrigger as Unstable_RegisteredTrigger,
|
|
506
519
|
type TriggerMatch,
|
|
507
520
|
type TriggerMatcher,
|
|
521
|
+
/** @deprecated Experimental since 2026-04-15. Not scheduled for removal; the API may change in any release. */
|
|
508
522
|
type TriggerBehavior as Unstable_TriggerBehavior,
|
|
509
523
|
} from "./primitives/composer/trigger";
|
|
510
524
|
import type {
|
|
@@ -584,7 +598,10 @@ export {
|
|
|
584
598
|
type Unstable_WebMcpProviderOptions,
|
|
585
599
|
type Unstable_WebMcpProviderResult,
|
|
586
600
|
} from "./unstable/webmcp/useWebMcpProvider";
|
|
587
|
-
export {
|
|
601
|
+
export {
|
|
602
|
+
/** @deprecated Experimental since 2026-08-29. Not scheduled for removal; the API may change in any release. */
|
|
603
|
+
defaultWebMcpFilter as unstable_defaultWebMcpFilter,
|
|
604
|
+
} from "./unstable/webmcp/convertTools";
|
|
588
605
|
|
|
589
606
|
// Shared surface carried by every distribution (scripts/check-distribution-barrels.mjs)
|
|
590
607
|
export type {
|
|
@@ -1,13 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
useLayoutEffect,
|
|
6
|
-
useMemo,
|
|
7
|
-
useRef,
|
|
8
|
-
useState,
|
|
9
|
-
type ReactNode,
|
|
10
|
-
} from "react";
|
|
3
|
+
import { useEffect, useMemo, useRef, useState, type ReactNode } from "react";
|
|
4
|
+
import { useIsomorphicLayoutEffect } from "../utils/useIsomorphicLayoutEffect";
|
|
11
5
|
import type {
|
|
12
6
|
McpAppMetadata,
|
|
13
7
|
TextMessagePart,
|
|
@@ -204,7 +198,7 @@ function InlineRenderer({
|
|
|
204
198
|
const opts = resolvePartOptions(rendererOptions, part);
|
|
205
199
|
const app = getMcpAppFromToolPart(part);
|
|
206
200
|
const cachedAppRef = useRef<McpAppMetadata | undefined>(undefined);
|
|
207
|
-
|
|
201
|
+
useIsomorphicLayoutEffect(() => {
|
|
208
202
|
if (app != null) cachedAppRef.current = app;
|
|
209
203
|
}, [app]);
|
|
210
204
|
const appForRender = app ?? cachedAppRef.current;
|
|
@@ -31,6 +31,44 @@ describe("McpAppFrame", () => {
|
|
|
31
31
|
cleanup();
|
|
32
32
|
});
|
|
33
33
|
|
|
34
|
+
it("enforces the resource CSP before rendering widget HTML", () => {
|
|
35
|
+
let sandboxProps: SandboxHostProps | undefined;
|
|
36
|
+
sandboxHostMock.mockImplementation((props: SandboxHostProps) => {
|
|
37
|
+
sandboxProps = props;
|
|
38
|
+
return null;
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
render(
|
|
42
|
+
<McpAppFrame
|
|
43
|
+
app={{ resourceUri: "ui://example/widget" }}
|
|
44
|
+
resource={{
|
|
45
|
+
uri: "ui://example/widget",
|
|
46
|
+
mimeType: MCP_APP_MIME_TYPE,
|
|
47
|
+
html: '<!doctype html><script src="https://cdn.example/app.js"></script>',
|
|
48
|
+
meta: {
|
|
49
|
+
csp: {
|
|
50
|
+
connectDomains: ["https://api.example"],
|
|
51
|
+
resourceDomains: ["https://cdn.example"],
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
}}
|
|
55
|
+
/>,
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
expect(sandboxProps?.content.html).toContain(
|
|
59
|
+
'http-equiv="Content-Security-Policy"',
|
|
60
|
+
);
|
|
61
|
+
expect(sandboxProps?.content.html).toContain(
|
|
62
|
+
"connect-src 'self' https://api.example",
|
|
63
|
+
);
|
|
64
|
+
expect(sandboxProps?.content.html).toContain(
|
|
65
|
+
"script-src 'self' 'unsafe-inline' https://cdn.example",
|
|
66
|
+
);
|
|
67
|
+
expect(
|
|
68
|
+
sandboxProps?.content.html.indexOf("Content-Security-Policy"),
|
|
69
|
+
).toBeLessThan(sandboxProps?.content.html.indexOf("<script") ?? 0);
|
|
70
|
+
});
|
|
71
|
+
|
|
34
72
|
it("keeps bridge options scoped to committed renders", async () => {
|
|
35
73
|
let committedCreateBridge: SandboxHostProps["createBridge"] | null = null;
|
|
36
74
|
sandboxHostMock.mockImplementation((props: SandboxHostProps) => {
|
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
useEffect,
|
|
6
|
-
useLayoutEffect,
|
|
7
|
-
useRef,
|
|
8
|
-
} from "react";
|
|
3
|
+
import { type MutableRefObject, useEffect, useRef } from "react";
|
|
4
|
+
import { useIsomorphicLayoutEffect } from "../utils/useIsomorphicLayoutEffect";
|
|
9
5
|
import { type McpAppBridge, createMcpAppBridge } from "./bridge";
|
|
10
6
|
import {
|
|
11
7
|
SandboxHost,
|
|
@@ -19,6 +15,7 @@ import type {
|
|
|
19
15
|
McpAppHostContext,
|
|
20
16
|
} from "./types";
|
|
21
17
|
import { isRecord } from "@assistant-ui/core/internal";
|
|
18
|
+
import { applyMcpAppCsp } from "./csp";
|
|
22
19
|
|
|
23
20
|
const DEFAULT_PRODUCT = "assistant-ui-mcp-app";
|
|
24
21
|
const INIT_TIMEOUT_MS = 5000;
|
|
@@ -190,7 +187,7 @@ export function McpAppFrame({
|
|
|
190
187
|
input,
|
|
191
188
|
output,
|
|
192
189
|
});
|
|
193
|
-
|
|
190
|
+
useIsomorphicLayoutEffect(() => {
|
|
194
191
|
liveRef.current = {
|
|
195
192
|
handlers,
|
|
196
193
|
hostInfo,
|
|
@@ -313,7 +310,7 @@ export function McpAppFrame({
|
|
|
313
310
|
|
|
314
311
|
return (
|
|
315
312
|
<SandboxHost
|
|
316
|
-
content={{ html: resource.html }}
|
|
313
|
+
content={{ html: applyMcpAppCsp(resource.html, resource.meta?.csp) }}
|
|
317
314
|
contentKey={
|
|
318
315
|
app.serverId ? `${app.serverId} ${resource.uri}` : resource.uri
|
|
319
316
|
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
import { applyMcpAppCsp, buildMcpAppCsp } from "./csp";
|
|
5
|
+
|
|
6
|
+
const getPolicyMeta = (html: string) =>
|
|
7
|
+
new DOMParser()
|
|
8
|
+
.parseFromString(applyMcpAppCsp(html), "text/html")
|
|
9
|
+
.head.querySelector('meta[http-equiv="Content-Security-Policy"]');
|
|
10
|
+
|
|
11
|
+
const expectPolicyBeforeScript = (html: string) => {
|
|
12
|
+
const policyIndex = html.indexOf("Content-Security-Policy");
|
|
13
|
+
const scriptIndex = html.indexOf("<script");
|
|
14
|
+
expect(policyIndex).toBeGreaterThanOrEqual(0);
|
|
15
|
+
expect(scriptIndex).toBeGreaterThanOrEqual(0);
|
|
16
|
+
expect(policyIndex).toBeLessThan(scriptIndex);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
describe("MCP App CSP", () => {
|
|
20
|
+
it("uses restrictive defaults when the resource omits CSP metadata", () => {
|
|
21
|
+
expect(buildMcpAppCsp()).toBe(
|
|
22
|
+
"default-src 'none'; " +
|
|
23
|
+
"script-src 'self' 'unsafe-inline'; " +
|
|
24
|
+
"style-src 'self' 'unsafe-inline'; " +
|
|
25
|
+
"connect-src 'none'; " +
|
|
26
|
+
"img-src 'self' data:; " +
|
|
27
|
+
"font-src 'self'; " +
|
|
28
|
+
"media-src 'self' data:; " +
|
|
29
|
+
"frame-src 'none'; " +
|
|
30
|
+
"base-uri 'self'; " +
|
|
31
|
+
"object-src 'none'; " +
|
|
32
|
+
"form-action 'none'",
|
|
33
|
+
);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("allows only valid declared origins", () => {
|
|
37
|
+
const policy = buildMcpAppCsp({
|
|
38
|
+
connectDomains: [
|
|
39
|
+
"https://api.example",
|
|
40
|
+
"wss://events.example:8443",
|
|
41
|
+
"https://api.example; default-src *",
|
|
42
|
+
],
|
|
43
|
+
resourceDomains: ["https://*.cdn.example", "wss://assets.example"],
|
|
44
|
+
frameDomains: ["https://video.example/path"],
|
|
45
|
+
baseUriDomains: ["https://base.example"],
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
expect(policy.split("; ")).toContain(
|
|
49
|
+
"connect-src 'self' https://api.example wss://events.example:8443",
|
|
50
|
+
);
|
|
51
|
+
expect(
|
|
52
|
+
buildMcpAppCsp({
|
|
53
|
+
resourceDomains: ["https://*.cdn.example", "wss://assets.example"],
|
|
54
|
+
}),
|
|
55
|
+
).toContain("script-src 'self' 'unsafe-inline' https://*.cdn.example");
|
|
56
|
+
expect(
|
|
57
|
+
buildMcpAppCsp({ resourceDomains: ["wss://assets.example"] }),
|
|
58
|
+
).toContain("script-src 'self' 'unsafe-inline';");
|
|
59
|
+
expect(
|
|
60
|
+
buildMcpAppCsp({ frameDomains: ["https://video.example/path"] }),
|
|
61
|
+
).toContain("frame-src 'none'");
|
|
62
|
+
expect(
|
|
63
|
+
buildMcpAppCsp({ baseUriDomains: ["https://base.example"] }),
|
|
64
|
+
).toContain("base-uri https://base.example");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("falls back to the restrictive policy for malformed metadata", () => {
|
|
68
|
+
expect(
|
|
69
|
+
buildMcpAppCsp({
|
|
70
|
+
connectDomains: "https://api.example" as unknown as string[],
|
|
71
|
+
}),
|
|
72
|
+
).toContain("connect-src 'none'");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("places the policy after a doctype and before executable content", () => {
|
|
76
|
+
const html =
|
|
77
|
+
'\uFEFF<!-- license --><!doctype html><script src="https://cdn.example/app.js"></script>';
|
|
78
|
+
const secured = applyMcpAppCsp(html);
|
|
79
|
+
|
|
80
|
+
expect(secured.startsWith("<!-- license --><!doctype html><meta ")).toBe(
|
|
81
|
+
true,
|
|
82
|
+
);
|
|
83
|
+
expectPolicyBeforeScript(secured);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it.each([
|
|
87
|
+
"<!--><script>run()</script>-->",
|
|
88
|
+
"<!---><script>run()</script>",
|
|
89
|
+
"<!-- license --!><script>run()</script>",
|
|
90
|
+
])("places the policy before scripts after HTML comment closers", (html) => {
|
|
91
|
+
expectPolicyBeforeScript(applyMcpAppCsp(html));
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it.each([
|
|
95
|
+
'<?xml version="1.0"?><!doctype html><script>run()</script>',
|
|
96
|
+
"<!foo><!doctype html><script>run()</script>",
|
|
97
|
+
])("preserves standards mode after leading bogus comments", (html) => {
|
|
98
|
+
const secured = applyMcpAppCsp(html);
|
|
99
|
+
const document = new DOMParser().parseFromString(secured, "text/html");
|
|
100
|
+
|
|
101
|
+
expect(secured.indexOf("<!doctype html><meta ")).toBeGreaterThan(0);
|
|
102
|
+
expect(document.compatMode).toBe("CSS1Compat");
|
|
103
|
+
expectPolicyBeforeScript(secured);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it.each([
|
|
107
|
+
"\u00a0<script>run()</script>",
|
|
108
|
+
"\uFEFF\uFEFF<script>run()</script>",
|
|
109
|
+
])("keeps the policy in head before non-HTML whitespace", (html) => {
|
|
110
|
+
expect(getPolicyMeta(html)).not.toBeNull();
|
|
111
|
+
});
|
|
112
|
+
});
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type { McpAppResourceCSP } from "./types";
|
|
2
|
+
|
|
3
|
+
const isAllowedOrigin = (value: string, allowWebSocket: boolean): boolean => {
|
|
4
|
+
const scheme = allowWebSocket ? /^(?:https?|wss?):\/\//i : /^https?:\/\//i;
|
|
5
|
+
if (!scheme.test(value) || /[\s;'"`,]/.test(value)) {
|
|
6
|
+
return false;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
try {
|
|
10
|
+
const url = new URL(value.replace("://*.", "://wildcard."));
|
|
11
|
+
return (
|
|
12
|
+
url.username === "" &&
|
|
13
|
+
url.password === "" &&
|
|
14
|
+
url.pathname === "/" &&
|
|
15
|
+
url.search === "" &&
|
|
16
|
+
url.hash === ""
|
|
17
|
+
);
|
|
18
|
+
} catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const getAllowedOrigins = (
|
|
24
|
+
values: unknown,
|
|
25
|
+
allowWebSocket = false,
|
|
26
|
+
): string[] =>
|
|
27
|
+
Array.isArray(values)
|
|
28
|
+
? [
|
|
29
|
+
...new Set(
|
|
30
|
+
values.filter(
|
|
31
|
+
(value): value is string =>
|
|
32
|
+
typeof value === "string" &&
|
|
33
|
+
isAllowedOrigin(value, allowWebSocket),
|
|
34
|
+
),
|
|
35
|
+
),
|
|
36
|
+
]
|
|
37
|
+
: [];
|
|
38
|
+
|
|
39
|
+
const directive = (name: string, values: readonly string[]) =>
|
|
40
|
+
`${name} ${values.join(" ")}`;
|
|
41
|
+
|
|
42
|
+
export const buildMcpAppCsp = (csp?: McpAppResourceCSP): string => {
|
|
43
|
+
const connectDomains = getAllowedOrigins(csp?.connectDomains, true);
|
|
44
|
+
const resourceDomains = getAllowedOrigins(csp?.resourceDomains);
|
|
45
|
+
const frameDomains = getAllowedOrigins(csp?.frameDomains);
|
|
46
|
+
const baseUriDomains = getAllowedOrigins(csp?.baseUriDomains);
|
|
47
|
+
|
|
48
|
+
return [
|
|
49
|
+
"default-src 'none'",
|
|
50
|
+
directive("script-src", ["'self'", "'unsafe-inline'", ...resourceDomains]),
|
|
51
|
+
directive("style-src", ["'self'", "'unsafe-inline'", ...resourceDomains]),
|
|
52
|
+
directive(
|
|
53
|
+
"connect-src",
|
|
54
|
+
connectDomains.length > 0 ? ["'self'", ...connectDomains] : ["'none'"],
|
|
55
|
+
),
|
|
56
|
+
directive("img-src", ["'self'", "data:", ...resourceDomains]),
|
|
57
|
+
directive("font-src", ["'self'", ...resourceDomains]),
|
|
58
|
+
directive("media-src", ["'self'", "data:", ...resourceDomains]),
|
|
59
|
+
directive("frame-src", frameDomains.length > 0 ? frameDomains : ["'none'"]),
|
|
60
|
+
directive(
|
|
61
|
+
"base-uri",
|
|
62
|
+
baseUriDomains.length > 0 ? baseUriDomains : ["'self'"],
|
|
63
|
+
),
|
|
64
|
+
"object-src 'none'",
|
|
65
|
+
"form-action 'none'",
|
|
66
|
+
].join("; ");
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const findDoctypeEnd = (html: string): number => {
|
|
70
|
+
let offset = 0;
|
|
71
|
+
|
|
72
|
+
while (offset < html.length) {
|
|
73
|
+
const whitespace = html.slice(offset).match(/^[\t\n\f\r ]+/)?.[0];
|
|
74
|
+
if (whitespace) {
|
|
75
|
+
offset += whitespace.length;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (html.startsWith("<!--", offset)) {
|
|
79
|
+
if (html.startsWith("<!-->", offset)) {
|
|
80
|
+
offset += 5;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (html.startsWith("<!--->", offset)) {
|
|
84
|
+
offset += 6;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
const commentEnd = html.indexOf("-->", offset + 4);
|
|
88
|
+
const bangCommentEnd = html.indexOf("--!>", offset + 4);
|
|
89
|
+
if (commentEnd === -1 && bangCommentEnd === -1) return 0;
|
|
90
|
+
offset =
|
|
91
|
+
bangCommentEnd !== -1 &&
|
|
92
|
+
(commentEnd === -1 || bangCommentEnd < commentEnd)
|
|
93
|
+
? bangCommentEnd + 4
|
|
94
|
+
: commentEnd + 3;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (html.slice(offset, offset + 9).toLowerCase() === "<!doctype") {
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
if (html.startsWith("<?", offset) || html.startsWith("<!", offset)) {
|
|
101
|
+
const bogusCommentEnd = html.indexOf(">", offset + 2);
|
|
102
|
+
if (bogusCommentEnd === -1) return 0;
|
|
103
|
+
offset = bogusCommentEnd + 1;
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (html.slice(offset, offset + 9).toLowerCase() !== "<!doctype") {
|
|
110
|
+
return offset;
|
|
111
|
+
}
|
|
112
|
+
const end = html.indexOf(">", offset + 9);
|
|
113
|
+
return end === -1 ? 0 : end + 1;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
export const applyMcpAppCsp = (
|
|
117
|
+
html: string,
|
|
118
|
+
csp?: McpAppResourceCSP,
|
|
119
|
+
): string => {
|
|
120
|
+
const normalizedHtml = html.charCodeAt(0) === 0xfeff ? html.slice(1) : html;
|
|
121
|
+
const meta = `<meta http-equiv="Content-Security-Policy" content="${buildMcpAppCsp(csp)}">`;
|
|
122
|
+
const insertionPoint = findDoctypeEnd(normalizedHtml);
|
|
123
|
+
return `${normalizedHtml.slice(0, insertionPoint)}${meta}${normalizedHtml.slice(insertionPoint)}`;
|
|
124
|
+
};
|
package/src/mcp-apps/types.ts
CHANGED
|
@@ -237,6 +237,51 @@ describe("AssistantFrame Integration", () => {
|
|
|
237
237
|
unsubscribe();
|
|
238
238
|
});
|
|
239
239
|
|
|
240
|
+
it("rejects a tool call the frame drops while it is disposed", async () => {
|
|
241
|
+
vi.mocked(iframeWindow.postMessage).mockImplementation((data: any) => {
|
|
242
|
+
Promise.resolve().then(() => {
|
|
243
|
+
messageHandlers.get("iframe")?.({
|
|
244
|
+
data,
|
|
245
|
+
source: parentWindow,
|
|
246
|
+
origin: window.location.origin,
|
|
247
|
+
} as MessageEvent);
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
vi.mocked(window.removeEventListener).mockImplementation(
|
|
251
|
+
(event: string, handler: any) => {
|
|
252
|
+
if (event === "message" && messageHandlers.get("iframe") === handler) {
|
|
253
|
+
messageHandlers.delete("iframe");
|
|
254
|
+
}
|
|
255
|
+
},
|
|
256
|
+
);
|
|
257
|
+
const registry = new ModelContextRegistry();
|
|
258
|
+
const toolExecute = vi.fn().mockResolvedValue("result");
|
|
259
|
+
registry.addTool({
|
|
260
|
+
toolName: "search",
|
|
261
|
+
parameters: z.object({}),
|
|
262
|
+
execute: toolExecute,
|
|
263
|
+
});
|
|
264
|
+
AssistantFrameProvider.addModelContextProvider(registry);
|
|
265
|
+
const host = new AssistantFrameHost(iframeWindow);
|
|
266
|
+
await vi.waitFor(() => {
|
|
267
|
+
expect(host.getModelContext().tools?.search).toBeDefined();
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
const result = host.getModelContext().tools!.search!.execute!(
|
|
271
|
+
{},
|
|
272
|
+
{} as any,
|
|
273
|
+
);
|
|
274
|
+
AssistantFrameProvider.dispose();
|
|
275
|
+
|
|
276
|
+
await expect(result).rejects.toThrow(
|
|
277
|
+
"AssistantFrameProvider has been disposed",
|
|
278
|
+
);
|
|
279
|
+
expect(toolExecute).not.toHaveBeenCalled();
|
|
280
|
+
expect(host.getModelContext().tools).toBeUndefined();
|
|
281
|
+
|
|
282
|
+
host.dispose();
|
|
283
|
+
});
|
|
284
|
+
|
|
240
285
|
it("should handle multiple providers", async () => {
|
|
241
286
|
// Setup multiple providers
|
|
242
287
|
const registry1 = new ModelContextRegistry();
|
|
@@ -78,6 +78,7 @@ All messages are wrapped with a channel identifier to avoid conflicts with other
|
|
|
78
78
|
2. **Tool Execution**
|
|
79
79
|
- `tool-call`: Parent requests tool execution in iframe (where tools are defined)
|
|
80
80
|
- `tool-result`: Iframe returns execution result or error to parent
|
|
81
|
+
- `provider-disposed`: Iframe tells the parent it was disposed, so the parent rejects tool calls the iframe will not answer
|
|
81
82
|
|
|
82
83
|
#### Serialization
|
|
83
84
|
|
|
@@ -7,6 +7,7 @@ import { useAuiEvent } from "@assistant-ui/store";
|
|
|
7
7
|
|
|
8
8
|
export namespace AssistantModalPrimitiveRoot {
|
|
9
9
|
export type Props = PopoverPrimitive.PopoverProps & {
|
|
10
|
+
/** @deprecated Experimental since 2024-10-12. Not scheduled for removal; the API may change in any release. */
|
|
10
11
|
unstable_openOnRunStart?: boolean | undefined;
|
|
11
12
|
};
|
|
12
13
|
}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
export { AttachmentPrimitiveRoot as Root } from "./attachment/AttachmentRoot";
|
|
2
|
-
export {
|
|
2
|
+
export {
|
|
3
|
+
/** @deprecated Experimental since 2024-10-10. Not scheduled for removal; the API may change in any release. */
|
|
4
|
+
AttachmentPrimitiveThumb as unstable_Thumb,
|
|
5
|
+
} from "./attachment/AttachmentThumb";
|
|
3
6
|
export { AttachmentPrimitiveName as Name } from "./attachment/AttachmentName";
|
|
4
7
|
export { AttachmentPrimitiveRemove as Remove } from "./attachment/AttachmentRemove";
|