Skip to content

Restore crypto.randomUUID before the Studio bundle evaluates - #9075

Open
Lyxot wants to merge 2 commits into
unslothai:mainfrom
Lyxot:fix/studio-crypto-randomuuid-boot
Open

Restore crypto.randomUUID before the Studio bundle evaluates#9075
Lyxot wants to merge 2 commits into
unslothai:mainfrom
Lyxot:fix/studio-crypto-randomuuid-boot

Conversation

@Lyxot

@Lyxot Lyxot commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator

Fixes #9046

The bug

Studio's web UI renders a blank page when it is reached over plain http:// at a LAN address — unsloth studio -H 0.0.0.0, then opening http://<LAN-IP>:<PORT> from another machine. The console shows:

Uncaught TypeError: crypto.randomUUID is not a function
    <anonymous> http://<LAN-IP>:<PORT>/assets/settings-*.js

crypto.randomUUID() is a secure-context API, so it is absent on a page served over plain http from a LAN address. localhost is a secure context by definition, which is why local runs never hit this.

Root cause

The frontend has 67 crypto.randomUUID() references, and exactly one of them runs at module scope rather than inside a function:

// studio/frontend/src/features/chat/stores/chat-runtime-store.ts
const threadSettingsWriter = crypto.randomUUID();

main.tsx already carried a randomUUID polyfill, but in the module body. A module's static imports are evaluated before its own body, and the path from the entry to that store is entirely static (main.tsxapp/appapp/routerroutes/chatfeatures/chatchat-runtime-store, where routes/chat.tsx uses a plain import rather than lazyRouteComponent). So the throwing module always evaluated first and the polyfill could never precede it, wherever in main.tsx it was placed. The throw kills the entry chunk, React never mounts, and the page stays blank.

The fix

Move the polyfill out of the bundle and into an external classic script loaded from index.html ahead of the module entry, which is the one position that runs before module evaluation. The generator itself is unchanged from the one previously in main.tsx.

External rather than inline because the backend sends script-src 'self' with no 'unsafe-inline' and no nonce on normal requests, so an inline bootstrap is blocked. This mirrors the existing public/theme-boot.js, which is an external file for exactly the same reason. Files in public/ are emitted to the build root and served by the frontend catch-all, the same path theme-boot.js already takes in production.

Reviewer notes and trade-offs

  • Ordering. The module entry is deferred, so any non-async script above it runs first. defer on the boot scripts would preserve that too, since deferred classic scripts and the module entry share one execution list in document order; async on either would not. The test asserts the tags are non-async rather than forbidding defer.
  • The other 66 call sites were left alone. With the polyfill installed ahead of the module graph, bare crypto.randomUUID() is correct everywhere, including at module scope. Seven files that hand-roll their own fallback are now redundant, but removing them is a behavior-neutral refactor and is better done separately than mixed into this fix.
  • The UUID test is a golden value, not a randomness test. The generator is a pure function of the bytes it draws, so pinning its output for a fixed stream catches the ways it can quietly degrade — bits masked off, draws folded together, or the stream ignored. It certifies no entropy, and a deliberate rewrite of the generator is expected to change the value.
  • Scope. This makes Studio boot over plain http. It does not make plain http a fully working deployment: dictation, microphone capture and parts of the clipboard API remain unavailable outside a secure context by browser design, and the code already guards those. unsloth studio --secure remains the supported path for full remote access.

Validation

Built the frontend, served the build over a LAN address, and loaded it in a browser with window.isSecureContext === false.

Before, with the boot script omitted from the same build:

Uncaught TypeError: crypto.randomUUID is not a function
    at assets/settings-*.js
#root child elements: 0

After:

typeof crypto.randomUUID === "function"
#root child elements: 3   (login screen renders)

Inline was also checked against the running backend rather than assumed — an inline polyfill in the served index.html is rejected:

Executing inline script violates the following Content Security Policy directive 'script-src 'self''.
Uncaught TypeError: crypto.randomUUID is not a function

Each added assertion was mutation-checked, and each of these fails exactly one test: removing the script tag, commenting it out, moving it below the module entry, marking either tag async, changing the nibble mask 15 to 14, dropping the mask, replacing the source with Math.random, folding the drawn bytes together, reusing one byte, and overwriting an existing randomUUID. defer on the boot script alone still passes, as intended.

cd studio/frontend
npm run typecheck
node --experimental-strip-types --test tests/crypto-uuid-boot.test.ts

Studio rendered a blank page when reached over plain http on a LAN address. `crypto.randomUUID()` is secure-context only, and the chat runtime store calls it at module scope, so the bundle threw before React mounted. `localhost` is a secure context, which is why local runs never saw it.

`main.tsx` already carried a polyfill, but in the module body: a module's static imports evaluate before its own body, so the throwing module always ran first and the polyfill could never precede it. Move it to an external classic script loaded from `index.html` ahead of the module entry, the one position that runs before module evaluation. External rather than inline because the backend CSP is `script-src 'self'`, following the existing `theme-boot.js` pattern.

Secure-context features such as dictation and clipboard remain unavailable over plain http by browser design; `--secure` stays the supported path for full remote access.
@Lyxot

Lyxot commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Hooray!

Reviewed commit: 7f21be1169

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

The preview iframe is sandboxed with an opaque origin, so it has its own globals and the page's boot script cannot reach it. Canvas code calling `crypto.randomUUID()` therefore threw over plain http on a LAN address — the same secure-context gap the app itself hit — and the canvas rendered nothing.

Install the fallback in the frame shell, before the message listener that renders the canvas: the first render replaces the document and takes that listener with it. It is inlined rather than sharing the page's `crypto-boot.js` because the strict frame CSP allows no external script at all; a test pins the two copies to each other so they cannot drift, since only the shared one has a golden test over its output.

The dev smoke pages load the boot script too, so every HTML entry is covered, and the markup tests iterate all of them rather than `index.html` alone.
@Lyxot

Lyxot commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator Author

Added a second commit extending the fix to the artifact preview canvas.

The preview iframe is sandboxed with an opaque origin, so it has its own globals and the page's boot script cannot reach it. Canvas code calling crypto.randomUUID() hit the same secure-context gap. Reproduced over plain http on a LAN address before the change (THREW: crypto.randomUUID is not a function, isSecureContext === false) and verified working after.

Two behaviours were measured rather than assumed: a fallback installed in the shell survives the document.open() in render() and reaches the canvas's own scripts, so installing once is enough; and the frame renders exactly once, because document.open() removes the window's message listener — which is why the install has to precede it.

It is inlined rather than sharing crypto-boot.js because the strict frame CSP is default-src 'none'; script-src 'unsafe-inline', so no external script can load there. A test pins the two copies to each other, since only the shared one has a golden test over its output.

The dev smoke pages also load the boot script now, so every HTML entry is covered, and the markup tests iterate all entries instead of index.html alone.

Note this part is a pre-existing limitation rather than part of the #9046 regression — canvases never had randomUUID over plain http. Happy to split it out if you would rather keep this PR to the regression alone.

@codex review

@Lyxot
Lyxot requested a review from danielhanchen as a code owner August 17, 2026 09:35
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Something went wrong. Try again later by commenting “@codex review”.

Unknown error
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@Lyxot

Lyxot commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. What shall we delve into next?

Reviewed commit: a9e3468562

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] crypto.randomUUID crashes Studio web UI over non-secure HTTP

1 participant