Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
2649521
web: sandbox hardening — opaque-origin inner frame, srcdoc-only, fram…
cliffhall Jul 11, 2026
446f6ba
web: enforce per-app _meta.ui.csp in the app bridge + surface resourc…
cliffhall Jul 11, 2026
679a3e2
web: full hostContext delivery + live host-context-changed updates
cliffhall Jul 11, 2026
326aa0e
web: honor ui/notifications/size-changed and ui/request-display-mode
cliffhall Jul 11, 2026
8f120dc
web: honor ui/download-file with confirmation and http(s) allowlist
cliffhall Jul 11, 2026
77af567
web: surface ui/message and app logging in panels below the running app
cliffhall Jul 11, 2026
9551a3f
web: stage partial input — replay tool-input-partial before final too…
cliffhall Jul 11, 2026
908adf9
web: app lifecycle status + machine-readable data attributes on the A…
cliffhall Jul 11, 2026
cdd166d
review: warn when server sandbox drops allow-scripts + revert inciden…
cliffhall Jul 11, 2026
8437a1c
Merge branch 'v2/1565-sandbox-hardening' into v2/1566-per-app-csp
cliffhall Jul 11, 2026
345395b
review: document CSP-vs-permissions filtering asymmetry (PR #1652)
cliffhall Jul 11, 2026
226c2c6
test: lock in per-app HOST_CAPABILITIES isolation (PR #1652 round 2)
cliffhall Jul 11, 2026
a03a074
Merge branch 'v2/1566-per-app-csp' into v2/1567-hostcontext-delivery
cliffhall Jul 11, 2026
0426f1b
review: gate theme observer on initialized + document seed/dep assump…
cliffhall Jul 11, 2026
5688b6b
Merge branch 'v2/1567-hostcontext-delivery' into v2/1568-size-display…
cliffhall Jul 11, 2026
0c30294
review: guard 0-height size reports, distinct RendererFrame, shared p…
cliffhall Jul 11, 2026
9ffaa42
Merge branch 'v2/1568-size-displaymode' into v2/1569-download-file
cliffhall Jul 11, 2026
a3a5bfa
review: distinguish links, cap batch size, comment truncation (PR #1657)
cliffhall Jul 11, 2026
ca21c59
review: skip embedded resource with neither text nor blob (PR #1657 r…
cliffhall Jul 11, 2026
f646322
Merge branch 'v2/1569-download-file' into v2/1570-message-logs
cliffhall Jul 11, 2026
0190220
review: type-honest formatLogData, cap channels, log-panel a11y (PR #…
cliffhall Jul 11, 2026
e8e0a5c
review: stable id key for Messages panel (PR #1658 round 2)
cliffhall Jul 11, 2026
16fd1ea
Merge branch 'v2/1570-message-logs' into v2/1571-stage-partial-input
cliffhall Jul 11, 2026
541d2c4
review: note partialStages intentionally survives handleOpen (PR #1659)
cliffhall Jul 11, 2026
4ccfc3c
Merge branch 'v2/1571-stage-partial-input' into v2/1572-app-status
cliffhall Jul 12, 2026
6b097ac
review: align error-panel docs with behavior + gate data-app-error on…
cliffhall Jul 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions clients/web/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,23 @@ Components live under `src/components/` in four layers, smallest to largest:

Every screen and element has a `*.stories.tsx` (see [Storybook](#storybook)). Styling follows the Mantine-first rules in [`AGENTS.md`](../../AGENTS.md) — theme variants and component props over CSS, `--inspector-*` tokens over raw colors.

## MCP Apps screen automation contract

The Apps screen exposes a small, stable set of `data-testid` / `data-*` attributes so an automated driver (deep-link auto-open, CI review harness) can `waitForSelector` on a deterministic signal instead of sleeping. Treat these as a public contract — drivers depend on them staying stable:

| Attribute | Where | Meaning |
| --- | --- | --- |
| `data-testid="apps-form"` | Apps content card | The container that carries the status/error attributes below. |
| `data-app-status` | on `apps-form` | Renderer lifecycle: `idle` (nothing running) → `loading` (bridge building / `ui/initialize` in flight) → `ready` (view fired `notifications/initialized`) → `error` (bridge factory threw/rejected). Poll for `ready`. |
| `data-app-error` | on `apps-form` | The failure reason string when `data-app-status="error"` (e.g. no connected client); absent otherwise. |
| `data-testid="apps-error"` | error panel | Rendered below the frame when the app fails to load (factory throw/reject); shows the reason so the failure isn't a silent blank frame. |
| `data-testid="open-app"` | Open App button | Launches the selected app. |
| `data-testid="apps-stage"` | Stage-partial button | Snapshots the current form values for progressive-render testing. |
| `data-testid="apps-messages"` | messages panel | `ui/message` submissions from the running view. |
| `data-testid="apps-logs"` | app-logs panel | `notifications/message` log entries (default-expanded). |

The renderer lifecycle itself is `AppRendererStatus` (`loading` | `ready` | `error`) reported via `AppRenderer`'s `onAppStatusChange`; the screen maps it to `data-app-status`. Resource-read failures (malformed/404 UI resource) are surfaced as a toast via the bridge factory's `onResourceError`; because the app never reaches `ready` in that case, a driver times out on `data-app-status` and reads the toast.

## Theme (`src/theme/`)

Each customized Mantine component has a `Theme<Name>.ts` file (`Button.ts`, `Text.ts`, …, ~21 total) exporting a `Theme<Name>` constant; the barrel `index.ts` re-exports them and `theme.ts` assembles the `MantineProvider` theme. Theme files hold app-wide defaults and **variants** (flat CSS-in-JS); only pseudo-selectors, nested child selectors, keyframes, and native-HTML styling belong in `App.css`. Element components import from `@mantine/core` (never from `theme/`) — the theme layer is applied transparently by the provider.
Expand Down
13 changes: 13 additions & 0 deletions clients/web/server/sandbox-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,18 @@ export function createSandboxController(
let server: Server | null = null;
let sandboxUrl: string | null = null;

// Defense-in-depth for the proxy page itself. Only `frame-ancestors` is set
// here — fetch directives (`default-src`, `connect-src`, etc.) are
// deliberately omitted because a `srcdoc` iframe clones its embedder's CSP
// policy container: any fetch directive on this header would be inherited by
// the inner app document and, since multiple CSPs intersect, would override
// the per-app `connect-src`/`img-src` allowlists the host bakes into the
// wrapped HTML (see src/lib/sandbox-csp.ts). The opaque-origin sandbox on
// the inner frame is the structural boundary; `frame-ancestors` ensures the
// proxy can only be embedded by the local inspector itself.
const SANDBOX_PROXY_CSP =
"frame-ancestors http://127.0.0.1:* http://localhost:*";

let sandboxHtml: string;
try {
const sandboxHtmlPath = join(__dirname, "../static/sandbox_proxy.html");
Expand Down Expand Up @@ -91,6 +103,7 @@ export function createSandboxController(
"Content-Type": "text/html; charset=utf-8",
"Cache-Control": "no-store, no-cache, must-revalidate",
Pragma: "no-cache",
"Content-Security-Policy": SANDBOX_PROXY_CSP,
});
res.end(sandboxHtml);
});
Expand Down
13 changes: 13 additions & 0 deletions clients/web/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -708,6 +708,19 @@ function App() {
const invocation = await inspectorClient.readResource(uri);
return invocation.result;
},
// The bridge's sandboxready handler reads + posts the UI resource
// inside a detached async block; without this hook a 404 / malformed
// resource is console.error-only and the user stares at a blank
// frame. Surface it as a toast. The renderer separately drives
// `data-app-status` so an automated driver can time out on
// never-reaching-"ready" and read the toast.
onResourceError: (err) => {
notifications.show({
title: "App resource failed to load",
message: err.message,
color: "red",
});
},
}),
[inspectorClient],
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ function createMockBridge(): AppBridge {
sendToolInput: async () => {},
sendToolResult: async () => {},
sendToolCancelled: async () => {},
sendHostContextChange: async () => {},
addEventListener: () => {},
removeEventListener: () => {},
teardownResource: async () => ({}),
close: async () => {},
} as unknown as AppBridge;
Expand Down
Loading
Loading