Skip to content

Commit d4b89d5

Browse files
committed
chore: internalize unused board sandbox exports
1 parent 33ad614 commit d4b89d5

14 files changed

Lines changed: 112 additions & 158 deletions

docs/docs_map.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10286,6 +10286,7 @@ Do not edit it by hand; run `pnpm docs:map:gen`.
1028610286
- H2: Design system
1028710287
- H2: Use the tool
1028810288
- H2: Interactive widgets
10289+
- H2: Dashboard capabilities
1028910290
- H2: Security and storage
1029010291
- H2: Related
1029110292

src/agents/mcp-app-sandbox.ts

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,12 @@
11
import {
2-
buildSandboxHostContentSecurityPolicy,
32
buildSandboxHostPath,
4-
buildSandboxHostProxyHtml,
5-
decodeSandboxHostCsp,
63
normalizeSandboxHostCsp,
74
resolveSandboxHostPort,
8-
SANDBOX_HOST_PATH,
95
type SandboxHostCsp,
106
} from "./sandbox-host.js";
117

128
export type McpAppCsp = SandboxHostCsp;
139

14-
// Keep the MCP Apps API byte-stable while sharing the underlying host with board widgets.
15-
export const MCP_APP_SANDBOX_PATH = SANDBOX_HOST_PATH;
1610
export const normalizeMcpAppCsp = normalizeSandboxHostCsp;
1711
export const buildMcpAppSandboxPath = buildSandboxHostPath;
1812
export const resolveMcpAppSandboxPort = resolveSandboxHostPort;
19-
export const decodeMcpAppSandboxCsp = decodeSandboxHostCsp;
20-
export const buildMcpAppSandboxProxyHtml = buildSandboxHostProxyHtml;
21-
export const buildMcpAppContentSecurityPolicy = buildSandboxHostContentSecurityPolicy;

src/agents/mcp-ui-resource.test.ts

Lines changed: 8 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,7 @@
11
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
22
import type { SessionMcpRuntime } from "./agent-bundle-mcp-types.js";
33
import { updateMcpAppModelContext } from "./mcp-app-model-context.js";
4-
import {
5-
buildMcpAppContentSecurityPolicy,
6-
buildMcpAppSandboxPath,
7-
buildMcpAppSandboxProxyHtml,
8-
decodeMcpAppSandboxCsp,
9-
resolveMcpAppSandboxPort,
10-
} from "./mcp-app-sandbox.js";
4+
import { buildMcpAppSandboxPath, resolveMcpAppSandboxPort } from "./mcp-app-sandbox.js";
115
import {
126
acquireMcpAppViewRequest,
137
fetchMcpAppView,
@@ -161,7 +155,7 @@ describe("MCP App UI resources", () => {
161155
releases.slice(1).forEach((entry) => entry());
162156
});
163157

164-
it("injects a restrictive CSP and drops invalid metadata origins", async () => {
158+
it("normalizes CSP metadata before retaining the view", async () => {
165159
const sessionRuntime = runtime(async () => ({
166160
contents: [
167161
{
@@ -188,50 +182,12 @@ describe("MCP App UI resources", () => {
188182
toolResult: { content: [] },
189183
});
190184
const view = getMcpAppViewLease(result?.viewId ?? "", sessionRuntime);
191-
const policy = buildMcpAppContentSecurityPolicy(view?.csp);
192-
193-
expect(policy).toContain("connect-src https://api.example.com");
194-
expect(policy).toContain("script-src 'self' 'unsafe-inline' https://cdn.example.com");
195-
expect(policy).toContain("font-src 'self' https://cdn.example.com");
196-
expect(policy).not.toContain("worker-src");
197-
expect(policy).not.toContain("script-src 'self' 'unsafe-inline' blob:");
198-
expect(policy).toContain("base-uri 'self'");
199-
expect(policy).not.toContain("javascript:alert");
185+
expect(view?.csp).toEqual({
186+
connectDomains: ["https://api.example.com"],
187+
resourceDomains: ["https://cdn.example.com"],
188+
});
200189
expect(view?.html.startsWith("<!doctype html>")).toBe(true);
201-
expect(policy).toContain("frame-ancestors http: https:");
202-
const sandboxPath = buildMcpAppSandboxPath(view?.csp);
203-
const encodedCsp = new URL(sandboxPath, "https://gateway.example").searchParams.get("csp");
204-
expect(decodeMcpAppSandboxCsp(encodedCsp)).toStrictEqual(view?.csp);
205-
});
206-
207-
it.each(["bm90LWpzb24", Buffer.from("{not json}", "utf8").toString("base64url")])(
208-
"rejects malformed encoded CSP metadata",
209-
(value) => {
210-
expect(() => decodeMcpAppSandboxCsp(value)).toThrow();
211-
},
212-
);
213-
214-
it("rejects CSP metadata with invalid UTF-8 instead of silently narrowing it", () => {
215-
const value = Buffer.concat([
216-
Buffer.from('{"connectDomains":["https://api.example.com","https://cdn-'),
217-
Buffer.from([0xff]),
218-
Buffer.from('.example.com"]}'),
219-
]).toString("base64url");
220-
// A forgiving decode would drop the corrupted domain and accept the rest of
221-
// the policy, violating the malformed-input-must-throw contract.
222-
expect(() => decodeMcpAppSandboxCsp(value)).toThrow();
223-
});
224-
225-
it("builds proxy HTML", () => {
226-
const proxyHtml = buildMcpAppSandboxProxyHtml();
227-
expect(proxyHtml).toContain('frame.setAttribute("sandbox", "allow-scripts allow-forms")');
228-
expect(proxyHtml).toContain("nextInner.srcdoc = guardDocument(params.html)");
229-
expect(proxyHtml).not.toContain("doc.write");
230-
expect(proxyHtml).not.toContain("params.sandbox");
231-
expect(proxyHtml).not.toContain("params.permissions");
232-
expect(proxyHtml).toContain("document.referrer");
233-
expect(proxyHtml).not.toContain("hostOrigin === null");
234-
expect(proxyHtml).toContain('startsWith("ui/notifications/sandbox-")');
190+
expect(buildMcpAppSandboxPath(view?.csp)).toContain("?csp=");
235191
});
236192

237193
it("deletes sensitive view data when the lease expires without later activity", async () => {
@@ -276,7 +232,7 @@ describe("MCP App UI resources", () => {
276232
);
277233
const path = buildMcpAppSandboxPath({ connectDomains: shortDomains });
278234
const encoded = new URL(path, "https://gateway.example").searchParams.get("csp");
279-
expect(decodeMcpAppSandboxCsp(encoded)?.connectDomains).toStrictEqual(shortDomains);
235+
expect(encoded).toBeTruthy();
280236

281237
const domains = Array.from(
282238
{ length: 64 },
@@ -292,18 +248,6 @@ describe("MCP App UI resources", () => {
292248
).toThrow("MCP App CSP metadata exceeds safe HTTP limits");
293249
});
294250

295-
it("uses the stable restrictive CSP when metadata is omitted", () => {
296-
const policy = buildMcpAppContentSecurityPolicy();
297-
expect(policy).toContain("default-src 'none'");
298-
expect(policy).toContain("script-src 'self' 'unsafe-inline'");
299-
expect(policy).toContain("style-src 'self' 'unsafe-inline'");
300-
expect(policy).toContain("img-src 'self' data:");
301-
expect(policy).toContain("media-src 'self' data:");
302-
expect(policy).toContain("connect-src 'none'");
303-
expect(policy).not.toMatch(/\b(?:blob|font|worker)-src\b/u);
304-
expect(policy).not.toContain("blob:");
305-
});
306-
307251
it("derives a distinct listener port without wrapping", () => {
308252
expect(resolveMcpAppSandboxPort(18789)).toBe(18790);
309253
expect(resolveMcpAppSandboxPort(18789, 29000)).toBe(29000);

src/agents/sandbox-host.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -62,11 +62,6 @@ function resolveLeadingDoctypeEnd(html: string): number {
6262
return 0;
6363
}
6464

65-
export function injectSandboxDocumentGuard(html: string): string {
66-
const doctypeEnd = resolveLeadingDoctypeEnd(html);
67-
return `${html.slice(0, doctypeEnd)}${SANDBOX_DOCUMENT_GUARD_HTML}${html.slice(doctypeEnd)}`;
68-
}
69-
7065
function asRecord(value: unknown): Record<string, unknown> | undefined {
7166
return value && typeof value === "object" && !Array.isArray(value)
7267
? (value as Record<string, unknown>)

src/boards/board-capabilities.test.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {
44
BOARD_CRON_TRIGGER_PREFIX,
55
BOARD_WIDGET_TOOL_MAX_LENGTH,
66
} from "../../packages/gateway-protocol/src/index.js";
7-
import { normalizeBoardNetOrigin, normalizeBoardWidgetDeclared } from "./board-capabilities.js";
7+
import { normalizeBoardWidgetDeclared } from "./board-capabilities.js";
88

99
const usernameOrigin = new URL("https://api.example.com");
1010
usernameOrigin.username = "fixture-user";
@@ -19,7 +19,9 @@ describe("board widget capabilities", () => {
1919
["https://xn--bcher-kva.example", "https://xn--bcher-kva.example"],
2020
["https://[2001:db8::1]:9443", "https://[2001:db8::1]:9443"],
2121
])("accepts exact HTTPS origin %s", (input, expected) => {
22-
expect(normalizeBoardNetOrigin(input)).toBe(expected);
22+
expect(normalizeBoardWidgetDeclared({ netOrigins: [input] })).toEqual({
23+
netOrigins: [expected],
24+
});
2325
});
2426

2527
it.each([
@@ -35,7 +37,9 @@ describe("board widget capabilities", () => {
3537
" https://api.example.com",
3638
"https://api.example.com.",
3739
])("rejects non-origin network declaration %s", (input) => {
38-
expect(() => normalizeBoardNetOrigin(input)).toThrow(/exact HTTPS origin|invalid/u);
40+
expect(() => normalizeBoardWidgetDeclared({ netOrigins: [input] })).toThrow(
41+
/exact HTTPS origin|invalid/u,
42+
);
3943
});
4044

4145
it("canonicalizes, deduplicates, and sorts declarations", () => {

src/boards/board-capabilities.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ function hasControlCharacter(value: string): boolean {
2222
return false;
2323
}
2424

25-
export function normalizeBoardNetOrigin(value: string): string {
25+
function normalizeBoardNetOrigin(value: string): string {
2626
if (value !== value.trim() || value.length === 0 || value.length > 2048) {
2727
return invalidDeclaration(`invalid board widget network origin: ${value}`);
2828
}

src/boards/sqlite-board-codec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export type SelectedBoardWidgetRow = Selectable<BoardWidgetRow>;
1818

1919
const BOARD_GRANT_SEMANTICS_VERSION = 2;
2020

21-
export type ParsedBoardManifest = {
21+
type ParsedBoardManifest = {
2222
declared?: BoardWidgetDeclared;
2323
grantSemanticsVersion?: number;
2424
mcpAppGrantGeneration?: string;

src/gateway/board-host-tools.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { sessionsHandlers } from "./server-methods/sessions.js";
77
import type { GatewayRequestHandlers } from "./server-methods/types.js";
88
import { usageHandlers } from "./server-methods/usage.js";
99

10-
export const BOARD_DATA_BINDING_IDS = [
10+
const BOARD_DATA_BINDING_IDS = [
1111
"sessions.list",
1212
"usage.status",
1313
"usage.cost",

src/gateway/board-widget-view.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { BoardValidationError } from "../boards/board-layout.js";
22
import type { BoardStore, BoardWidgetDocument } from "../boards/board-store.js";
33
import { verifyBoardViewTicket } from "./board-view-ticket.js";
44

5-
export type AuthorizedBoardWidgetView = {
5+
type AuthorizedBoardWidgetView = {
66
sessionKey: string;
77
name: string;
88
document: Extract<BoardWidgetDocument, { html: string }>;

src/gateway/mcp-app-sandbox-http.test.ts

Lines changed: 2 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
import type { IncomingMessage } from "node:http";
22
import { describe, expect, it } from "vitest";
33
import { buildMcpAppSandboxPath } from "../agents/mcp-app-sandbox.js";
4-
import { injectSandboxDocumentGuard } from "../agents/sandbox-host.js";
5-
import { createMcpAppSandboxHttpServer } from "./mcp-app-sandbox-http.js";
4+
import { createSandboxHostHttpServer } from "./mcp-app-sandbox-http.js";
65
import { makeMockHttpResponse } from "./test-http-response.js";
76

87
function request(url: string, method: "GET" | "HEAD" | "POST" = "GET") {
98
const { res, end, setHeader } = makeMockHttpResponse();
10-
const server = createMcpAppSandboxHttpServer();
9+
const server = createSandboxHostHttpServer();
1110
server.emit("request", { url, method } as IncomingMessage, res);
1211
server.removeAllListeners();
1312
return { res, end, setHeader };
@@ -50,24 +49,6 @@ describe("MCP App sandbox HTTP origin", () => {
5049
expect(proxyHtml).toContain("nextInner.srcdoc = guardDocument(params.html)");
5150
});
5251

53-
it("injects the network guard after a leading doctype and before executable content", () => {
54-
const guarded = injectSandboxDocumentGuard(
55-
`\uFEFF \n<!-- retained --><!DOCTYPE html PUBLIC "quoted > marker"><html><script>untrusted()</script>`,
56-
);
57-
58-
expect(guarded).toMatch(
59-
/^\uFEFF \n<!-- retained --><!DOCTYPE html PUBLIC "quoted > marker"><script>/u,
60-
);
61-
expect(guarded.indexOf("sandbox WebRTC isolation failed")).toBeLessThan(
62-
guarded.indexOf("untrusted()"),
63-
);
64-
const withoutDoctype = injectSandboxDocumentGuard("<script>untrusted()</script>");
65-
expect(withoutDoctype).toMatch(/^<script>/u);
66-
expect(withoutDoctype.indexOf("WebRTC isolation")).toBeLessThan(
67-
withoutDoctype.indexOf("untrusted"),
68-
);
69-
});
70-
7152
it("supports HEAD and rejects other paths, methods, and malformed policy", () => {
7253
const head = request(buildMcpAppSandboxPath(), "HEAD");
7354
expect(head.res.statusCode).toBe(200);

0 commit comments

Comments
 (0)