Sasha Apps sandbox origin
The Milestone 1 web host uses a separate-origin proxy iframe, which creates one
inner iframe with an opaque data: origin for the immutable app resource.
Production startup requires:
SASHA_PUBLIC_ORIGIN=https://studio.example.com
SASHA_APP_SANDBOX_ORIGIN=https://views.example.com
Both values must be absolute HTTPS origins with no trailing slash, path,
credentials, query, fragment or wildcard. The hostnames must differ, including
after case and trailing-dot normalization. A different port on the Studio
hostname does not isolate cookies and is rejected. MCP_PUBLIC_ORIGIN remains
MCP/OAuth-specific and does not configure this boundary.
Hostnames are validated after URL normalization, so encoded wildcards such ashttps://%2a.example.com are also rejected.
Point the sandbox hostname at the same application listener with its own valid
TLS certificate. The reverse proxy must preserve the original Host and set
the correct forwarded protocol. If X-Forwarded-Host is present, it must contain
one matching hostname and port. X-Forwarded-Proto, when present, must be exactly
the lowercase configured scheme (https in production); lists, conflicting
values and malformed tokens are rejected. Forwarding headers are accepted only
when Express trusts the immediate peer. Production composition trusts one proxy
hop: deployment ingress must restrict the application listener to that proxy,
and the proxy must replace client-supplied forwarding headers. Do not configure domain-wide Studio cookies
that include the sandbox hostname. No sandbox response sets cookies or reflects
CORS headers.
For development, the exact Studio origin http://localhost:3007 maps tohttp://127.0.0.1:3007 on the same listener. Other local ports require explicit
origin configuration; the built-in mapping reserves only port 3007, so other
loopback listeners retain their existing Studio behavior. The proxy is served by the Node listener; Vite's UI-only
development server is not a sandbox server.
The sandbox hostname serves only /app-sandbox and the content-versioned/app-sandbox/sasha-app-sandbox.<sha256>.js referenced by that shell. All other
paths terminate with 404 before Studio middleware. Studio refuses the sandbox
paths, including the unbundled /sasha-app-sandbox.js source path.
Studio sets the outer iframe's referrerPolicy="origin" explicitly. This
overrides Studio's global Helmet Referrer-Policy: no-referrer for that navigation
and supplies the exact Studio origin needed by the proxy's embedding check.
No Studio path, query, credential or app HTML is placed in the sandbox URL or
referrer.
The shell has no inline JavaScript and injects only exact validated origins as
script data attributes. Its CSP starts with default-src 'none', denies
connections, forms, workers, objects, fonts and media, permits only the local
proxy script, inline scripts/styles, data images, and opaque data: inner
frame. Inline script/style permission is the outer ceiling required bydata: CSP inheritance: the subsequently delivered resource's CSP adds exact
script/style hashes. Both policies apply, so the resource policy restricts the
app to its assembled code. No policy permits unsafe-eval or external domains.frame-ancestors permits only the configured Studio origin.
The relay preserves ext-apps 1.7.5 sandbox-ready/resource-ready methods, pins both
window references and origins, and validates forwarded envelopes, requests,
notifications and correlated results with exported MCP/ext-apps schemas. It
rejects additional resources, device/domain grants and sandbox overrides. The
outer and inner sandboxes are exactly allow-scripts allow-same-origin allow-forms with an
empty permission allow attribute. Forms need this token on every ancestor for
their JavaScript submit handlers to run; both CSP policies still deny actual
form navigation through form-action 'none'. A data: document retains an opaque origin
even with allow-same-origin, so app code cannot access the proxy DOM or execute
code in its realm. The relay accepts origin null only from the pinned inner
WindowProxy. Sending to that opaque target requires targetOrigin: '*'; Studio
messages still use the exact Studio origin. The outer frame-src data: prevents
the app from navigating that pinned window onto the proxy's origin.
Host-side app capability authorization remains
the AppBridge's responsibility.
Milestone 1 still mounts exactly one app view at a time. Browser-enforced opaque
realms replace the original shared-origin app/proxy design. Enabling concurrent
views remains future work requiring lifecycle, capability and UI verification.
Final assembled HTML, including the SDK and inlined assets, is limited to 1 MiB
of UTF-8. The server returns the existing safe APP_RESOURCE_LIMIT failure;
HTTP keeps its existing hidden-not-found envelope. The proxy independently
rejects oversized or malformed-Unicode resources. UTF-8 base64 encoding expands
1,048,576 bytes to 1,398,104 characters; with the 36-character data-URL prefix,
the maximum URL is 1,398,140 characters. This leaves headroom below Chromium's
URL limit. Repeated local-image expansion is budgeted during assembly.
Edit claudecodeui/public/sasha-app-sandbox.js, then run:
cd claudecodeui
npm run build:app-sandbox
npm run check:app-sandbox
npm run test:app-sandbox:browser
Commit the generated server/apps/generated/appSandboxProxy.js with source
changes. The normal npm run build regenerates it. The check command fails on
drift without modifying the artifact. Runtime composition only reads those
inert bytes; it never launches a bundler or fetches dependencies.
The browser regression requires installed Playwright Chromium. It uses only
generated local fixtures and blocks unexpected network requests. Both normal
and exactly-1-MiB resources complete MCP initialization and prove that app code
cannot read/mutate proxy DOM, add proxy sibling frames, or forge proxy-source
messages; the inner hash CSP also blocks unhashed scripts.
